Ajax — Examples

Example 1 — A resolved request treated as success

A button reloads the cart totals. When the server returns an error status the panel fills with a confusing parse failure, and nothing is shown while the request is in flight. Explain both faults, then model the three outcomes. The endpoint path below is a placeholder; substitute your own.

async function refreshTotals() {
  const response = await fetch("/api/cart/totals");
  const totals = await response.json();          // a 500 body parsed as payload
  document.querySelector("[data-total]").textContent = formatMoney(totals.total);
}

Solution

async function refreshTotals() {
  const panel = document.querySelector("[data-total]");
  panel.textContent = "Loading…";

  try {
    const response = await fetch("/api/cart/totals");
    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    const totals = await response.json();
    if (typeof totals?.total !== "number") throw new Error("unexpected payload");
    panel.textContent = formatMoney(totals.total);
  } catch (error) {
    panel.textContent = "Could not load totals";
  }
}

fetch rejects only on a network failure, so a 404 or a 500 arrives as a resolved promise and the error page’s body is handed to the JSON parser — which is why the failure surfaces as a parse error that names the wrong problem. Checking response.ok turns it into a status you can act on. The second fault is state: with no loading and no error branch, the panel keeps whatever it showed before and looks broken rather than busy. Rendering three explicit outcomes — loading, ready, error — is the whole fix, and validating the parsed shape catches the case where a success response is still not the payload you expected.

Example 2 — A write the server receives as an empty body

A form sends a new address with POST and the server reports a missing body. The same request works from a tool. Say what is missing from the request, then send it correctly. The endpoint path is a placeholder; substitute your own.

async function saveAddress(address) {
  const response = await fetch("/api/addresses", {
    method: "POST",
    body: JSON.stringify(address),
  });
  return response.json();
}

Solution

async function saveAddress(address) {
  const response = await fetch("/api/addresses", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(address),
  });

  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

Without a content type, the server has no way to know how to decode the bytes, so a serialised object arrives as an unparseable or empty body — and because the request itself succeeded, the failure looks like a server bug rather than a client one. Setting Content-Type: application/json is what makes the body interpretable, and it pairs with JSON.stringify rather than with a form payload: a form submission should use FormData and no header, so the browser can set the multipart boundary itself.