A gotcha with the Fetch API

It’s rare that I use the Fetch API directly these days but every time I do there’s one behaviour that I forget about... HTTP errors

When using the Fetch API, HTTP errors don’t result in an error being thrown. Let's look at what can go wrong when we make a network request.

Network errors

Network errors happen when the roundtrip between the browser and the server cannot be completed. Possible reasons for this include:

  • The client’s network connection drops

  • An incorrect URL is used

  • The server goes down and cannot respond

If a network error occurs the Fetch API will throw an error 👍

try {
  const response = await fetch("https://myapi.com/user/1");
} catch (error) {
  // FetchError: ...
}

HTTP errors

HTTP responses from the server with status codes beginning 4xx and 5xx represent errors. Common HTTP error status codes you might encounter are:

  • 404 when a resource isn’t found

  • 403 when the client isn’t authorised to access the resource

  • 500 an unexpected failure happened on the server

If an HTTP error occurs the Fetch API will NOT throw an error 👎

try {
  const response = await fetch("https://myapi.com/user/1");

  // HTTP errors do not throw so you must parse the response
  if (!response.ok) {
    // We have a HTTP error
  }
} catch (error) {
  // We have a network error 
}

I find this to be a surprising behaviour that has the potential for uncaught runtime errors to lean into your application. So how can we handle HTTP errors gracefully?

How to handle HTTP errors manually

I like to create a custom error class that contains a status code and message that gets thrown on an HTTP error.

Fetch responses always contain a status code. Whether or not you have an error message and how you get that message will depend on the response the server sends. In the example below let's assume that the error message is sent back as plain text in the response body.

// HttpError.ts
export class HttpError extends Error {
  public readonly statusCode: number;

  constructor(statusCode: number, message: string) {
    super(`HTTP Error (${statusCode}): ${message}`);
    this.name = "HttpError";
    this.statusCode = statusCode;
  }
}

// fetch.ts
import { HttpError } from "./HttpError";

try {
  const response = await fetch("https://myapi.com/user/1");

  if (!response.ok) {
    await response.text().then(message => {
      throw new HttpError(response.status, message);
    });
  }

  // handle successful responses
}

But you should use a library

Handling HTTP errors manually with Fetch is a bit of a pain which is why I prefer to use a library. My library recommendations are:

  • Ky for a lightweight HTTP library

  • Axios for something a bit more full-featured

Both of these libraries throw a real error when receiving an HTTP error.

Thanks for reading ⭐️