This episode covers communication with servers: the AJAX concept, fetching data with fetch and response.json, sending data with the POST method along with headers, and handling HTTP status and errors correctly. You build a realistic API client using JSON data.

A page with only static HTML quickly feels limited. Modern applications display data that comes from a server — news, prices, user profiles — and refresh it without reloading the page. This technique is known as AJAX, and its modern JavaScript implementation is the Fetch API.
Episode 15 covers fetch from scratch to production-ready: fetching data with GET, reading JSON responses, sending data with POST along with the correct headers, and handling HTTP status and errors properly. You'll build a complete API client against a public endpoint.
One key concept that challenges your early understanding: fetch is asynchronous. The response isn't immediately available — you'll see the full mechanism in episode 16 about Promises.
Before AJAX, every data change meant reloading the whole page — slow and disruptive. AJAX broke that pattern: the page sends a request in the background, receives data, then updates only the parts that changed. JavaScript acts as the connector between the interface and the server.
fetch is the built-in browser API (and in Node.js since version 18) for doing this. Its simplest form:
const respons = await fetch("https://api.contoh.com/posts");
const data = await respons.json();
console.log(data);fetch(url) sends a GET request and returns an object waiting for the response. await respons.json() converts the JSON response body into a JavaScript object. The await keyword only applies inside an async function — you'll understand both fully in episode 16.
For practice, use a stable public endpoint, for example JSONPlaceholder. The complete structure with error handling:
async function ambilPosting() {
const respons = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const posting = await respons.json();
return posting;
}
const hasil = await ambilPosting();
console.log(hasil.title);async function ambilPosting() wraps the waiting logic. jsonplaceholder.typicode.com/posts/1 returns one JSON post object. hasil.title accesses its title. This is the basic pattern of every GET request in a real application: fetch, parse JSON, then use the data.
Node.js 18 and above has built-in fetch, so the example above can run directly in the terminal:
node --version
node -e "fetch('https://jsonplaceholder.typicode.com/posts/1').then(r => r.json()).then(d => console.log(d.title))"node -e "fetch(...) executes code without a file. Node 18+ uses the undici-based runtime fetch — not a third-party library. First check node --version to make sure the version is at least 18, then note that the result is the same as when run in the browser.
To send data, fetch accepts a second configuration object with method, headers, and body:
async function buatPosting() {
const respons = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Belajar Fetch API",
body: "Mengirim data ke server",
userId: 1,
}),
});
const hasil = await respons.json();
return hasil;
}
const dibuat = await buatPosting();
console.log(dibuat.id);method: "POST" turns the request into a data send. headers tells the server the body content is JSON, and body: JSON.stringify(...) converts the object into a JSON string. A server supporting JSON Placeholder responds with an object carrying a new id.
For HTML forms, the JSON format is often replaced with FormData, which handles encoding automatically:
async function kirimForm(data) {
const formData = new FormData();
formData.append("nama", data.nama);
formData.append("pesan", data.pesan);
const respons = await fetch("/api/kontak", {
method: "POST",
body: formData,
});
return respons.ok;
}
console.log(await kirimForm({ nama: "Arman", pesan: "Halo" }));new FormData() builds the body in a multipart format the server recognizes. When using FormData, don't set Content-Type manually — the browser adds the correct boundary. respons.ok is true for HTTP 2xx statuses.
The classic trap: fetch doesn't throw an error when the server responds with a 404 or 500 status. The code keeps running. You must check the status explicitly:
async function ambilDenganCek() {
const respons = await fetch("https://jsonplaceholder.typicode.com/posts/99999");
if (!respons.ok) {
throw new Error(`HTTP ${respons.status}: ${respons.statusText}`);
}
return respons.json();
}
try {
const data = await ambilDenganCek();
console.log(data);
} catch (error) {
console.error("Gagal:", error.message);
}if (!respons.ok) catches every status other than 2xx and throws an error carrying the status information. throw new Error raises an error that try...catch can catch. Only network errors — server unreachable, DNS failure — make fetch throw automatically.
It's important to distinguish two layers of errors:
fetch throws, for example a dropped connection.fetch doesn't throw; you must check respons.ok.The standard production pattern: check respons.ok, throw an error with the status, then catch it in try...catch to show a user-friendly message.
Warning
Remember the main trap: fetch does not throw an error on HTTP 4xx and 5xx statuses. If you don't check respons.ok, the application will silently accept an error page as normal data and show confusing results to users. Always check the status before parsing JSON.
Episode 15 equipped you with client-server communication: the AJAX concept, fetching data with fetch and response.json, sending data with POST along with headers and FormData, and handling HTTP status and errors with a respons.ok check.
Key takeaways:
fetch(url) sends a GET and returns an asynchronous response.await respons.json() converts a JSON body into a JavaScript object.method, headers, and body.JSON.stringify converts an object into a string for sending.fetch doesn't throw on 4xx and 5xx statuses — check respons.ok.fetch for running examples in the terminal.In the next episode 16 we'll cover Promises, async/await, and asynchronous error handling — the mechanism driving all waiting code, from fetch to setTimeout. You'll master .then, .catch, Promise.all, and async try/catch to write reliable asynchronous code.