JavaScript From Zero · 22 min · 170 XP

Fetch and JSON

Read and send JSON with fetch, treat a 404 or 500 as the failure it is, and show loading, empty and error states on the page.

JSON is the text format almost every web API speaks. It looks like a JavaScript object, but it's stricter: keys and strings need double quotes, and there are no comments, no trailing commas, no functions and no undefined. JSON.stringify(value) turns a value into JSON text, and JSON.parse(text) turns the text back into a value. JSON.parse throws a SyntaxError on anything that isn't valid JSON, such as "{'name': 'Ada'}" with single quotes.

javascript
const order = { id: 101, items: ["tent"], note: undefined, placed: new Date("2026-03-01") };

JSON.stringify(order);
// '{"id":101,"items":["tent"],"placed":"2026-03-01T00:00:00.000Z"}'
// undefined properties vanish; dates become text and stay text after parsing

// localStorage only stores strings, so JSON is how you keep an object there
localStorage.setItem("cart", JSON.stringify(cart));
const saved = JSON.parse(localStorage.getItem("cart") ?? "[]");

fetch(url) makes a request and gives back a promise of a response. Reading the body is a second wait, because the headers arrive before the body does: await response.json() reads the body and parses it as JSON. A body can only be read once.

The trap: fetch only rejects when the request can't be made at all (the network is down, the address is wrong). A 404 Not Found or a 500 server error still resolves, with a response whose ok is false and whose status says what happened. Code that goes straight to response.json() then treats the server's error message as if it were the data. Check response.ok and throw, so every failure lands in one catch.

Reading JSON, and sending it
async function loadProduct(id) {
  const response = await fetch(`/api/products/${id}`);
  if (!response.ok) throw new Error(`Product ${id}: ${response.status}`);
  return response.json();
}

async function addReview(productId, stars) {
  const response = await fetch("/api/reviews", {
    method: "POST",
    headers: { "Content-Type": "application/json" },   // tell the server it's JSON
    body: JSON.stringify({ productId, stars }),         // the body must be text
  });
  if (!response.ok) throw new Error(`Review not saved: ${response.status}`);
  return response.json();
}

On the page, a request has more than two outcomes. It starts loading, then it succeeds with data, succeeds with nothing (an empty list is an answer, not an error), or fails. Show each one: say Loading… and disable the button that started it, render the data or an honest empty message, and on failure say so in words a person understands. try/catch/finally fits exactly: finally re-enables the button whichever way it went.

One more thing goes wrong on real networks: responses don't always arrive in the order you asked. If someone searches for shell and then lamp, and the shell reply is slower, it lands last and replaces the right results. The capstone asks you to handle that: remember which request is the latest, and ignore any older reply.

Try it in a browser: open DevTools on any site, go to the Network panel, filter by Fetch/XHR, and click a request. The Response tab shows the JSON the page received, and the status column shows which requests failed.

Loading your workspace…