Error handling in the JavaScript Fetch API
Tags: javascript, fetch, webdev, frontend
Published: 2023-05-08 15:10:13

## What's up with fetch?

When working with fetch in JavaScript, something that is often overlooked is proper error handling from a response. That catch block may not be doing what you think it is doing! 

In the following code snippets, we will be covering an example from a React application of fetching a list of vendor names using the GET method, then setting them in state. The error handling applies to all scenarios using fetch in JavaScript, not just in React.

Let's start with the example below.
```js
/* Initializing vendors to an empty array */
const [vendors, setVendors] = React.useState([]);

/* useEffect runs on mount to fetch data */
React.useEffect(() => {
    /* Setting up the fetch call */
    const fetchVendors = async () => {
        const response = await fetch('http://localhost:9000/vendors');
        const responseJSON = await response.json();

        return responseJSON;
    };

    /**
     * Calling fetchVendors. The intent is to set the response (vendor names)
     * in React state using setVendors if the fetch is successful. If an error
     * occurs, we do not want to set the state and instead want to log the error. 
     **/
    fetchVendors()
      .then((response) => setVendors(response))
      .catch((error) => console.error('Logging Error', error));
  }, []);
```

At first glance, it may appear that if the response received from the fetch call does not give a 200 as the status code, it will fall into the catch block and log the error. That is incorrect though and not how fetch works. 

The fetch API returns a promise which either resolves or rejects. If it resolves, it will go into "then", and if it rejects, it will go into "catch". When a status code other than 200 is received, that does not mean that fetch had a bad response. An example of this could be if the database failed to connect. The backend could be configured to send a 500 status code with an error name and error message. Without additional configuration on our frontend, it doesn't know that this should go into the catch block. It simply assumes that we successfully receieved a response from the backend and **resolves** the promise. The [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch) also state that the fetch API will only **reject** when a network error occurs, rather than HTTP errors. That means that the user of fetch is responsible for setting up proper error handling. So how do we do that?

## Checking the ok boolean of the response

If we log the following response from the initial example:
```js
 const response = await fetch('http://localhost:9000/vendors');
```

This is what the resolved response may look like. This would differ based on several factors, such as status code, local testing, etc.

```js
{
    body: ReadableStream,
    bodyUsed: true,
    headers: Headers {},
    ok: true,
    redirected: false,
    status: 200,
    statusText: "OK",
    type: "cors",
    url: "http://localhost:9000/vendors"
}
```

When making the fetch call, the response received will contain a key named **ok**. This will be a boolean value. Anything other than a 200 status will set the value of **ok** to **false**. Knowing that, we can now add a check to the fetch call.

```js
React.useEffect(() => {
    const fetchVendors = async () => {
        const response = await fetch('http://localhost:9000/vendors');
        const responseJSON = await response.json();
        
        /** This will throw an error if the response status is anything other than 200 */
        if (!response.ok) {
            throw new Error('Something is broken!');
        }
        return responseJSON;
    };

    fetchVendors()
      .then((response) => setVendors(response))
      .catch((error) => console.error('Logging Error', error));
  }, []);
```

The above example would now go into the catch block if we received anything other than a 200 response and it logs the error of "Something is broken!".

## Checking the content-type of the response

What if the response we receive back is not of JSON type? It's possible that we receive an error prior to checking **ok** of the response when running the following line of code.

```js
const responseJSON = await response.json();
```

The response also contains **Headers**. Through the Headers, the content type received can be validated before processing it in any specific way.

```js
React.useEffect(() => {
    const fetchVendors = async () => {
        let error;
        const response = await fetch('http://localhost:9000/vendors');

        /** Checking the response headers for the content type */
        const isContentTypeJson = response.headers.get('content-type')?.includes('application/json');

        /** If content is not JSON, will not be processed as a JSON and sets the error variable */
        const responseData = isContentTypeJson
            ? await response.json()
            : error = 'Not a JSON!';

        if (!response.ok) {
            throw new Error('Something is broken!');
        }

        // throw an error if error variable contains a value
        if (error) {
            throw new Error(error);
        }
    
        return responseData;
    };
    
    fetchVendors()
        .then((response) => setVendors(response))
        .catch((error) => console.error('Logging Error', error));
}, []);
```


## Promise.resolve() and Promise.reject()

We can also use **Promise.resolve()** and **Promise.reject()** to control the flow of the fetch rather than throwing errors directly. Pretend we have our frontend connected to a backend and the backend is sending JSON objects with a status code, error name and error message. Assuming we want to keep that data to log on our frontend, we could use Promise.reject() to send this data as the error data of our catch block.

Example response JSON received from backend. This would be the response after response.json() resolves and is assuming our backend sends 500 as the status code:

```js
    {
        name: 'Database',
        message: 'Failed to connect to database',
        status: 500,
    } 
```

The example below allows for more robust error handling and logging if the response from the backend will be following a consistent format.

```js
React.useEffect(() => {
    const fetchVendors = async () => {
        const response = await fetch('http://localhost:9000/vendors');
        const isContentTypeJson = response.headers.get('content-type')?.includes('application/json');

        /** Handle error object from backend */
        const responseData = isContentTypeJson
            ? await response.json()
            : { status: response.status, statusText: response.statusText };

        if (!response.ok) {
            /** since a 500 was received from backend, the flow would enter here and we can manually reject the Promise. This will move into the "catch" */
            return Promise.reject(responseData);
        }

        /** If a 200 was received, the promise would resolve and flow moves to "then" */
        return Promise.resolve(responseData);
    };

    fetchVendors()
      .then((response) => setVendors(response))
      /** error in the below case would contain { name: 'Database', message: 'Failed to connect to database', status: 500 } */
      .catch((error) => console.error('Logging Error', error));
}, []);
```

## Wrap Up
There are a variety of ways that error handling with the fetch API can be handled. The examples given in this post are just a few and will change based on the needs in your application. The important piece to remember here is that it's not safe to assume that fetch will go into the catch block based on the HTTP status code! MDN Web Docs state that this will only happen with a network error, such as an invalid URL.

Happy error catching!