When fetch Fails

fetch rejects only when no answer arrives at all, so check response.ok for answers like 404 and 500, catch network errors and timeouts (AbortSignal.timeout) with try/catch, and turn each one into a message a person understands.

Journeyman · JavaScript · REST API · Web Development · Updated

After this lesson you can

  • Check response.ok and throw an error when an API answers with a failing status
  • Catch a network error or a timeout from fetch, using try/catch and AbortSignal.timeout

Key takeaways

  • fetch rejects only when there's no answer at all, such as no internet or a server that doesn't exist. A 404 or a 500 is still an answer, so fetch doesn't reject.
  • response.ok is true for a status from 200 to 299. Check it, and throw an error when it's false, before you read the body.
  • A network error reaches your catch block as a TypeError. Its message depends on the browser, so don't show it to people as it is.
  • fetch(url, { signal: AbortSignal.timeout(5000) }) gives up after 5 seconds and rejects with an error named TimeoutError.

The coffee shop's Wi-Fi drops halfway through a request. The API you rely on is down for maintenance. Someone types a breed with a typo. None of that is rare: it happens to real users every day, and a page that didn't plan for it shows a broken picture, a page stuck on Loading..., or nothing at all. Planning for failure is also what separates a class project that works on your laptop from one that works for whoever opens it.

The shelter's breed explorer from Getting Data with fetch worked every time so far. This lesson breaks it on purpose, three different ways, and shows how to catch each one.

Two ways an order goes wrong

Back at the restaurant's order window, an order can go wrong in two very different ways:

  1. You get an answer, and it's "no". "Sorry, we're out of that." Someone answered; you know what happened.
  2. You get no answer at all. The window is shut, or you never reached it because the road was closed.

A request is the same. A 404 or a 500 is the first kind: the server answered, with a status code that says it couldn't help. A network error (no internet, or a server that doesn't exist) is the second kind. Here's the surprise that catches almost everyone: fetch only rejects for the second kind. An answer of "no" is still an answer, so the promise is fulfilled.

A 404 is still an answer

Ask for a breed the API doesn't have:

const response = await fetch('https://dog.ceo/api/breed/unicorn/images/random');
console.log(response.status);
console.log(response.ok);

const data = await response.json();
console.log(data.message);
404
false
Breed not found (main breed does not exist)

No error was thrown. If the page had put data.message into an <img>, it would have tried to load a picture from the address Breed not found (main breed does not exist) and shown a broken image. Nothing in the Console would say why.

Check response.ok

response.ok is true when the status is from 200 to 299, and false for everything else. Check it straight after fetch, before you read the body, and throw your own error when it's false, as in Throwing Your Own Errors:

if (!response.ok) {
  throw new Error(`The dog API answered with status ${response.status}`);
}

! is read "not", so the line reads "if the response is not OK". Here it is inside the photo function, called once for a real breed and once for a made-up one:

async function randomPhotoOf(breed) {
  const response = await fetch(`https://dog.ceo/api/breed/${breed}/images/random`);
  if (!response.ok) {
    throw new Error(`The dog API answered with status ${response.status}`);
  }
  const data = await response.json();
  return data.message;
}

try {
  console.log(await randomPhotoOf('pug'));
  console.log(await randomPhotoOf('unicorn'));
} catch (error) {
  console.log(`Something went wrong: ${error.message}`);
}
https://images.dog.ceo/breeds/pug/n02110958_12807.jpg
Something went wrong: The dog API answered with status 404

Now a bad status is an error, and it lands in the same catch block as every other error (Handling Errors with try, catch and finally). The pug's address will be different when you run it: the photo is random.

No answer at all: network errors

The .invalid ending is reserved: no website will ever have it, so it's a safe way to ask a server that doesn't exist:

try {
  const response = await fetch('https://shelter-photos.invalid/dogs');
  console.log(response.status);
} catch (error) {
  console.log(error.name);
  console.log(error.message);
}
TypeError
fetch failed

This time fetch rejects, because no answer came back, so response.status is never printed. The error is a TypeError, and its message depends on where the code runs: Node says fetch failed, Chrome says Failed to fetch, and Firefox says NetworkError when attempting to fetch resource. None of those means anything to a visitor, so never show them as they are.

Try it. Open the shelter's test page from Getting Data with fetch with Live Preview, and in DevTools' Network tab change No throttling to Offline. Click the button: the Console shows the red TypeError. Set it back to No throttling afterward.

Don't wait forever: AbortSignal.timeout

Sometimes there's no error and no answer: the server is so slow that the request hangs. By default fetch waits for a very long time, and so does the person looking at your page. A timeout says "give up after this long". You pass it to fetch as a second argument, an object of options:

const response = await fetch(url, { signal: AbortSignal.timeout(5000) });

AbortSignal.timeout(5000) makes a signal that says "stop" after 5000 milliseconds, which is 5 seconds. To see it happen, give a request 1 millisecond, which is far too short for any server:

try {
  const response = await fetch('https://dog.ceo/api/breeds/list/all', {
    signal: AbortSignal.timeout(1),
  });
  console.log(response.status);
} catch (error) {
  console.log(error.name);
  console.log(error.message);
}
TimeoutError
The operation was aborted due to timeout

The error's name is TimeoutError, whatever the browser, so that's what your code checks. Five to ten seconds is a common timeout for a page: long enough for a slow phone connection, short enough that nobody stares at Loading... forever.

Put it together: a message for people

Each kind of failure deserves its own message, in words a visitor understands. The error's name tells them apart:

function friendlyMessage(error) {
  if (error.name === 'TimeoutError') {
    return 'The breed list is taking too long to load. Try again in a moment.';
  }
  if (error.name === 'TypeError') {
    return "We couldn't reach the breed list. Check your internet connection.";
  }
  return 'The breed list is having trouble right now. Try again later.';
}

async function breedCount(url) {
  try {
    const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
    if (!response.ok) {
      throw new Error(`The breed list answered with status ${response.status}`);
    }
    const data = await response.json();
    return `${Object.keys(data.message).length} breeds to explore`;
  } catch (error) {
    return friendlyMessage(error);
  }
}

console.log(await breedCount('https://dog.ceo/api/breeds/list/all'));
console.log(await breedCount('https://dog.ceo/api/breeds/list/everything'));
console.log(await breedCount('https://shelter-photos.invalid/breeds'));
108 breeds to explore
The breed list is having trouble right now. Try again later.
We couldn't reach the breed list. Check your internet connection.

Three requests, three different results, and the program never crashes. The second address has a typo in its path, so the API answers 404, and the error thrown for !response.ok gets the last message. One thing to know: a bug in your own code, such as calling something that isn't a function, is a TypeError too. On a real page, also log the real error with console.error(error), so you can see what happened in the Console while the visitor sees the friendly words.

Check yourself

2 questions to try on the lesson page.

So what

response.ok for answers that say "no", try/catch for no answer, and a timeout for an answer that never comes: with those three, your page tells people what went wrong instead of quietly breaking.

Key terms

Cheat sheets

Related