How to handle 4xx and 5xx errors in Javascript fetch()

Many modern javascript applications make use of the in-built fetch() API. This API takes care of most of the HTTP stuff and hence reduces the overhead of an external library like Axios or jQuery in many cases.

fetch makes use of Promise instead of callbacks to make things easier for the developers. For the sake of explanation, Let's fetch my github profile as an example and log to console. Typically, our fetch usage would look like this:

function fetchProfile(profileURL) {
    fetch(profileURL)
        .then(res => res.json())
        .then(userData => console.log('Response: ', userData))
}

fetchProfile('https://api.github.com/users/ahmedtabrez')
// This logs my profile in JSON format

This should work for a happy flow. Let's do some error handling now.

function fetchProfile(profileURL) {
    fetch(profileURL)
        .then(res => res.json())
        .then(userData => console.log('Success: ', userData))
        .catch(error => console.error('Error: ', error))
}

fetchProfile('https://non.sense.url/')

And this is what gets logged to my console:

image.png

Fair enough so far. Now let's try fetching my profile with a deliberate typo (let's add a hyphen at the end)

fetchProfile('https://api.github.com/users/ahmedtabrez-')

And here's what is logged: image.png

Wait a minute, why did it log Success: <object>? Wasn't it supposed to log Error: <object> according to our code just like it did for https://non.sense.url/?

Well, that's the caveat that makes using fetch slightly difficult.

According to the docs, fetch actually throws an exception only if there is a network failure or if something prevented the request from completing. It doesn't throw an exception for 4xx or 5xx responses

So in our case, fetchProfile('https://non.sense.url/') entered the catch block as expected because there was a network failure. There was actually no host at https://non.sense.url/. But when we did fetchProfile('https://api.github.com/users/ahmedtabrez-'), there was no network error. The URL we called took the request and responded with a response code. The request was complete and hence fetch did not consider that as an error.

If you have been using APIs like jQuery.ajax, you might expect the 4xx and 5xx errors in the catch block too. So let's write a wrapper function as a workaround for this.

To begin with:

function myFetch(...args) {
    return fetch(...args)
}

To handle the 4xx and 5xx responses, the fetch API, fortunately, provides us with a flag ok in the response object. We can make use of this flag to our advantage.

function myFetch(...args) {
    return fetch(...args)
        .then(res => {
            if (res.ok) {
                return res
            } else {
                throw new Error('An error occurred')
            }
        })
}

That would be enough. Now let's update our fetchProfile function and try again.

function fetchProfile(profileURL) {
    myFetch(profileURL)
        .then(res => res.json())
        .then(userData => console.log('Success: ', userData))
        .catch(error => console.error('Error: ', error))
}

fetchProfile('https://api.github.com/users/ahmedtabrez-')

And this is what the console looks like now: image.png As expected, the 404 response causes our fetchProfile to enter into the catch block.

A quick enhancement

In the network log in developer tools, The response body looks like image.png Let's say we want the server error response body within the catch block for handling the user experience for example by showing an error popup. But our implementation so far is only capable of giving us a generic error message An error occurred. To make this happen, we will make use of Promise.reject in place of throw like so:

function myFetch(...args) {
    return fetch(...args)        
        .then(res => {
            if (res.ok) {
                return res
            } else {
                // Assume our 4xx/5xx responses are always JSON objects for the sake of simplicity
                return res.json().then(json => Promise.reject(json))
            }
        })
}

Now let's see what fetchProfile('https://api.github.com/users/ahmedtabrez-') logs in console.

image.png

Now we have the error response body available at our catch block and we can make use of it as per our requirements.

Conclusion

There are tons of other implementations available over the internet and they could be more robust than this implementation. But as far as I was able to search, I could not find any implementation which gives us the response body in the catch block. That's what motivated me to write this post.

Let me know in the comments below if you feel like giving me any feedback. Also, let me know if you want me to write on a topic. I would love to explore.