# 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 November 1, 2026

## 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:

```javascript
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);
```

```text
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*:

```javascript
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:

```javascript
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}`);
}
```

```text
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:

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

```text
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:

```javascript
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:

```javascript
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);
}
```

```text
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:

```javascript
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'));
```

```text
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

- [Response Object](https://www.ask-codey.com/glossary/response-object)
- [404 Not Found](https://www.ask-codey.com/glossary/404-not-found)
- [500 Internal Server Error](https://www.ask-codey.com/glossary/500-internal-server-error)
- [catch Block](https://www.ask-codey.com/glossary/catch-block-javascript)
- AbortSignal.timeout()

## Cheat sheets

- [When fetch Fails](https://www.ask-codey.com/cheat-sheets/javascript-fetch-when-it-fails-cheat-sheet)

## Related

- [Getting Data with fetch](https://www.ask-codey.com/lessons/javascript-fetch-getting-data)
- [Handling Errors with try, catch and finally](https://www.ask-codey.com/lessons/javascript-errors-try-catch-finally)
- [Throwing Your Own Errors](https://www.ask-codey.com/lessons/javascript-errors-throwing-your-own)

Source: https://www.ask-codey.com/lessons/javascript-fetch-when-it-fails
