Centralizing API error handling in React apps

In this article I'm going to present to you a way of handling your API errors once and for all in a centralized and easily extendable way, regardless of the state-management library (Redux, Apollo, etc.) that you are using.

Almost every modern app interacts with an API for their data requirements; be it a RESTful API or a GraphQL one, firing requests on the web is the bread and butter of the majority of apps out there. Most of the times things go well and your request is returns successfully, but there are times when things don't go as smoothly. I'm talking about those dreaded 404s, 403s & 500s that the APIs can return, which in turn, have to be presented to the user. Due to the nature of these kind of responses, handling them all in a single place would be ideal.

Unfortunately, managing that in a React app has been harder than it should, with different completely approaches from app to app. In this article I'm going to present to you a way of handling your API errors once and for all in a centralized and easily extendable way, regardless of the state-management library (Redux, Apollo, etc.) that you are using. Moreover, the approach that we'll take can be re-used into all of your apps, regardless of whether you are implementing client or server-side rendering. So without further ado, let's dig straight in.

Setting the scene

For the purposes of this article, I'll assume the presence of a RESTful API with the usual HTTP error status codes, but similar concepts apply to a GraphQL and any other API. I'll also assume the use of react-router , but the exact same concepts apply for @reach/router or any other React routing library.

Let's say we want to write a small app that's going to have 2 pages. The first page is going to show us a list of dog breeds. When we click on a breed, we'll be redirected to this breed's page, which will display a random photo of a dog of that breed. The app would look like this (don't worry too much about the code or the UI, since it's outside the scope of this article) :

Our app works, but we haven't handled the 404 case yet. When the users visit a page that doesn't exist, we want to show them our very own 404 page with the text "Four:oh:four". This page should be shown in the following two scenarios:

  • The user visits an invalid URL regex (i.e. /cats/husky/).

  • The user visits a valid URL regex but with an invalid breed (this can happen if the user manually tweaks the URL from the address bar).

The first case is easy to handle. Like most tutorials out there recommend, we'll just add a "catch-all" route as the last route of our app and we will wrap all of our routes with react-router's <Switch /> component. This would look something like this:

Now whenever the user types in a URL other than / and /dogs/<BREED>, they will be redirected to the 404 page.

The second case is a bit harder to handle because it relies on our API. We don't know ahead of time, whether the breed that the user has entered as a URL parameter is valid or not, since we don't know all the breeds that exist in the world (or at least let's suppose that we don't). In order to show the 404 page, we have to wait for the server to respond and react (no pun intended) to the status that we got back. Modifying the <DogPage /> component, that would look something like that:

By storing the response code we can now react to its value and render different components. In the example above, if the status code was 404, we simply rendered the 404 page. This approach can work, but if you've used it before I can guarantee that you've had problems. To sum them up for you:

  • Handling 404 in a nested component
    It's easy to render <Page404 /> from a top-level component, but what will happen if this component is "deep" inside a component tree? Then <Page404 /> will render as part of a parent component, which means that not only will it not be full-screen, but that it will also allow a lot of other components to render their UI alongside it.

  • Repetitive code & logic
    Currently, we have handled 404 in one component, but we should do that for all of our components/pages that have an API call. That means a lot of repetitive code across multiple components.

  • Handling other error responses
    404 is just the most common one, but we should be able to do the same thing for 401, 403, 500, etc. That means that we should add even more code & logic to handle those cases.

  • Difficulty passing status as prop when API call is made outside of the component
    Here, the API call is performed inside the component, but in a redux scenario, this API call would happen inside a thunk, saga, or observable. How would we be able to pass that to our component in a clean, easy, one-off & non-persistent way?

A "redirect" solution

The easiest & most common thing people do, is simply redirect users to a /404 url where the <Page404 /> is rendered. This can work, but then the users lose context of where they are. They see a 404 page, but the URL that they have originally accessed has changed, so they don't know "which thing was not found" when they see a 404. What we want is a solution where the 404 page can be shown while the original url that they have accessed remains pristine.

A "hooks" solution

The first approach that we are going to take, is to use a re-usable custom hook to avoid having to re-write the handling of the API status code into every component. This hook (in a very basic implementation) would look something like this:

which then can easily be used in our <DogPage /> component like so:

This solved our repetitive logic when it comes to handling the status code, but it didn't help a lot with the repetitive logic of rendering the error page according to the status code. This still needs to be implemented in each component individually. In addition, hooks are not usable in class-based components, so if you have an older codebase, that might pose a problem.

A "render-props" solution

In order to allow for compatibility with class-based components and to reduce even further the amount of repetitive code, a render-props component can be helpful. Thus, using the useQuery hook that we created before, we can now create the following simplified version of a render-prop component:

which we can use in our <DogPage /> component like so:

Now this solution allows us to remove any repetitive logic, handle any HTTP error code, have compatibility with class-based components and overall have clean code. The only thing we haven't solved yet though, is how can we make sure that the <Page404 /> gets rendered in isolation. To recap, the 404 page is rendered in place of our DogPage component, but what happens if the DogPage is not a top-level component but has lots of other ancestors? Then the 404 page will be rendered as part of the entire UI, alongside other components.

A "top-level state" solution

To combat the last issue, our only option is to render the 404 page before any other page has the chance to render. For that we need some sort of state management library that will allow components lower down the component tree to notify a top-level component that it should show an error page (404, 403, 500, etc.). For that we can use any library, but let's go with the baked-in Context API.

What we need is a component that will make sure to render the proper error page based on the status code and will expose a way for any component to trigger this behavior (the showing of the error page). To implement it, we are gonna start by creating a high-level component that will expose the needed behavior via context. That might look something along the lines of this:

This component is a tiny bit complex, so please make sure to read the inline comments for more clarity.

To use this component, we are going to wrap our core logic with it like so:

In addition, we need to modify the useQuery hook to automatically "trigger" an error page display whenever a bad status code is returned. That would mean that the statusCode is no longer needed in our local state, since it's not managed by the ErrorHandler. All we need is to fire an action to set the error status code.

Finally, now that we have error handling in our ErrorHandler component, we now no longer need to react to error statuses in our components. Thus, our <DogPage /> component can take its final form, by stripping away any status checks:

So there you have it, a way to centralize error handling, through an easy (hooks), automated (no need to handle it in any future component) and scalable way (since you can handle any error you need in ErrorHandler).

Generic solution (last one, I promise)

The above solution works well, but it's not re-usable across all React projects. The thing is that the above approach works well when the fetching stays within the boundaries of React, but it becomes tedious when API calls are made in the redux world. How would you trigger the setErrorStatus from a redux thunk? You simply can't. What you'd need to do is trigger an action that would change the redux state and have ErrorHandler read from the redux state, but that would involve a lot of boilerplate code. Imagine the same in Apollo. That would involve using apollo-link-state and dispatching a mutation just for the freaking status code update.

With this in mind, let's take a step back and think what exactly are we trying to achieve here. We want to tie URLs to status codes and decide what to render based on the value of the code that's attached to them. Well, it turns out this is extremely easy by utilizing a browser native feature that not too many of us use.

The state of the current location.

On browser's native history API, each location can have a state key attached to it which can store anything we want. Why not utilize it to store the statusCode there instead of a local state? The bonus to this approach is that:

  • No state cleanup is needed since the moment you navigate away the location changes and thus the new location has a clean state.

  • The back button will automatically show you a 404 page without even an API request (since the previous location's state is stored in the browser's history stack).

  • Just because we assign state to a location in an imperative way, we can easily integrate this approach with any state management library.

  • Extremely configurable & scalable, since we can store & handle any key that we might want (not only the status code). This means that we can also store data that will be passed as props to our error page components.

With this in mind, we can simplify our ErrorHandler:

and any module in our system (regardless of whether it lives within React or not) can attach an error code to a page by taking the current location, adding a state to it and replacing the current history entry . To showcase that, we will modify our useQuery to do just that:

The benefit of this solution is that the useQuery hook is not even aware about the presence of the ErrorHandler component. All it does is modify the location as a side-effect, in order to add a certain state to the current history entry. That means that the ErrorHandler and the useQuery are not inter-dependent, so you can switch any of them out without any issues.

So there you have it. With just the ErrorHandler and the useQuery hook you now have a centralized API error handling module. It should be mentioned, that If you are using a state management library, chances are you won't be using the useQuery hook. Instead, you will have to manually trigger the history.replace as part of your HTTP fetching logic, which differs from library to library. Let's see how you would tackle that in some of the most popular ones:

For Redux users

To implement this solution in redux, you'll need to access your history. There are multiple ways of doing it, either by passing the history as a parameter to the createStore function and creating a middleware or by simply utilizing connected-react-router which exposes an action creator to modify your history directly. Simply fire this action with the correct params and you are good to go! The documentation has you more than covered on how can this be done.

For Apollo users

To handle that in the Apollo client you'll need to utilize apollo-link-error and create an error link that will have access to the history in order to call history.replace. The easiest way to do that, is to create an instance of history (through the createBrowserHistory if you are using react-router) and utilize it directly within the link. The same instance would need to be passed to the <Router> component. If you don't want to do that, another option would be to read the history from the useHistory hook, pass it as a parameter to the client (which would need to be created inside a react component) and then forward it to the error link. The latter is an approach you should consider only if you want to depend on the Router to provide a history instance, which would allow you to keep the same Apollo client for both your production and testing environment (due to dependency injection).

Closing notes

Although a lot of code that we've written in this article was not used at the end, it was necessary to showcase the problems with a lot of approaches I've seen out there. The goal of this article was to give you a nice way to handle API errors once and for all in all of your React apps. You can now forget about context, state, re-renders, hooks and everything. All you'll ever need again to handle your API errors is a top-level component that reads the current location's state and reacts accordingly, coupled with any sort of central "api module" that can modify the history. This solution is nice not only because it doesn't enforce component coupling, but because it's so abstract & generic that can easily be ported into any framework. So far this has played nicely in most of my apps and I'm really happy with it, but I'm curious to see what you think. The code for the final solution can be found here:

Please feel free to ask any questions or recommend even better ways of approaching this issue; I'd be glad to hear about them.

Thanks a lot for reading!

P.S. 👋 Hi, I’m Aggelos! If you liked this, consider following me on twitter or medium 😀