What you'll be able to do

By the end of this page you should be able to treat fetch failures in two buckets — network/CORS vs HTTP status — check response.ok, throw a clear error for bad statuses, and show a user-facing message without empty catch blocks.

Previous post: fetch API basics.
Related fundamentals: errors — try, catch, throw (Blog #22).

Who this is for

  • People whose UI shows “success” on 404 because they only awaited json()
  • Beginners who catch and ignore everything
  • Anyone building a small Studio-like client against an API

You can skip this if status checks are automatic for you. Come back when a 500 returns HTML and .json() blows up.

Two failure kinds

KindWhat happensHow you see it
Network / CORS / offlinePromise rejectstry/catch around fetch
HTTP 4xx / 5xxPromise fulfillsCheck res.ok / res.status

Scenario A — laptop offline. catch runs.

Scenario B — /api/user/999 → 404. fetch succeeds as a Response; ok is false.

A small helper pattern

async function fetchJson(url) {
  const res = await fetch(url);
  if (!res.ok) {
    throw new Error("HTTP " + res.status + " for " + url);
  }
  return res.json();
}

try {
  const data = await fetchJson("/api/items");
  console.log(data);
} catch (err) {
  console.error(err.message);
  // show err.message in the UI
}

Scenario A — happy path. ok true → parse JSON.

Scenario B — 500. Throw before trusting the body; surface the message.

Reading error bodies (optional)

Some APIs return { "error": "..." } on failure:

if (!res.ok) {
  let detail = "HTTP " + res.status;
  try {
    const body = await res.json();
    if (body.error) detail = body.error;
  } catch (_) {
    // body was not JSON
  }
  throw new Error(detail);
}

Scenario A — JSON error payload. Show the server message.

Scenario B — HTML error page. Fall back to status text; do not assume JSON.

Do not swallow errors

try {
  await fetchJson(url);
} catch (err) {
  // empty — worst habit
}

Scenario A — debug later. You lost the only clue.

Scenario B — users. Show a short message + keep a console error in development.

A tiny practice file

<!DOCTYPE html>
<html>
  <body>
    <button id="good">Good URL</button>
    <button id="bad">404 URL</button>
    <pre id="out"></pre>
    <script>
      async function fetchJson(url) {
        const res = await fetch(url);
        if (!res.ok) throw new Error("HTTP " + res.status);
        return res.json();
      }
      const out = document.getElementById("out");
      document.getElementById("good").addEventListener("click", async function () {
        try {
          const data = await fetchJson("https://jsonplaceholder.typicode.com/todos/1");
          out.textContent = JSON.stringify(data, null, 2);
        } catch (err) {
          out.textContent = err.message;
        }
      });
      document.getElementById("bad").addEventListener("click", async function () {
        try {
          await fetchJson("https://jsonplaceholder.typicode.com/todos/99999999");
        } catch (err) {
          out.textContent = err.message;
        }
      });
    </script>
  </body>
</html>

(Exact 404 behavior depends on the API — the !res.ok pattern still holds.)

Mistakes I see a lot

1. Only catching network errors. Status checks are separate.

2. Empty catch.

3. Parsing JSON before checking ok. Error pages may not be JSON.

4. Showing raw stack traces to end users. Log details; show short copy in UI.

What to try before the next post

  1. Add if (!res.ok) throw to your fetch helper.
  2. Compare offline failure vs 404 failure.
  3. Re-read Blog #22 for throw/catch vocabulary.
  4. Build the tiny HTML file.

Next in this series: Promise.all and friends — parallel vs sequential requests.

Try this next outside the series

Async code gets much easier when you see the same ideas on the browser side and the server side.