Error handling of an enterprise application in React

Masoud Bonabi
6 min readMar 5, 2023

--

We all probably know the importance of handling every error that is raised in an app. Especially if we know that we will be maintaining it for a long time.

There are some cool event-logging systems out there designed for receiving big data like event logs. These logs contain traces that exactly show where the error-producing code lives and how often they occur. They also can log the timing metrics for things like web vitals which are subject to another day. My favorite option (freely available if you have a good DevOps friend ;) ) is Sentry, but there are other options available.

So let’s see how immune is our codebase to every possible error in a production environment even in a legacy app.

Class-based solution with componentDidCatch

This is the first step and the most important one. Since most of our code lives inside the react lifecycle, it’s important to log all the errors of this kind and fix them as soon as possible. This kind of error is usually treated in the harshest way, breaking the user from continuing their flow and crashing the whole app.

What to show if the app crashes and catches

If you want to prevent the user from continuing their flow, you need to change the content and show some error page telling the user that something is not quite right and he/she needs to reload the tab. But sometimes a simple reload would not be enough to solve the issue and you can show a few other options that might also help the user: go to the homepage to start over or even logging out the user in order to clear all the data he/she has stored and making them start even more back!

import { Component } from "react";

export default class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { error: null };
}
componentDidCatch(error) {
this.setState({ error });
}
render() {
const { error } = this.state;
const { children } = this.props;

if (error) {
return (
<div>
{process.env.NODE_ENV !== "production" ? (
<pre>{error.stack}</pre>
) : (
"Please reload or go to homepage."
)}
</div>
);
} else {
return children;
}
}
}

It’s not perfect of course. But it does catch all the errors for me using the componentDidCatch method for me and then lets me render the error instead of the content if there are any. There is also another component that can be used for this purpose which is written by the Sentry team and of course it sends the error log to the configured server so that you can fix the issue later on having a cup of tea in your hand, wondering if there are any other users facing the same issue while you are having your tea!

Do we have the useEffect errors?

Yes, we do! The errors that are thrown inside our useEffect functions, happen through the React lifecycles, so React is aware of the error. And it would call our componentDidCatch function passing the error.

How about callbacks like onClick?

Well, the code above can handle any error that would happen inside React lifecycles. But these callback functions are run due to an event. Like scrolling a dom element with elementRef.current.addEventListener('scroll', myFunc) or passing a function as a prop, like: onClick={handleClick}.

These functions do not execute as a part of the React lifecycles (although they might trigger a state update) and if we want to catch these errors we’re gonna need extra try{} catch(e) {} blocks. The good news is, event-logging libraries like Sentry, already read all the errors that are shown on the browser’s console. So we can only rely on that.

How to handle API errors?

OK, so we have access to the failing requests if we were using a logging service like Sentry. But it won’t help the user fix the issue and continue the business flow. Imagine the user is scrolling through a list of infinite items with the pagination done by the backend, and all of a sudden, the user loses internet connectivity. What should he/she see? I think some kind of feedback would be nice to let him/her know that there is a failed request. But you know what would be even better? A retry option!

How to retry a failed request

Depending on your infrastructure for handling requests (e.g redux, ReactQuery or etc.) there are solutions to have some kind of retry option. Let’s see how we could implement it for each of these infrastructures.

1. Redux

First of all, we need to have a mechanism to determine if the dispatched action indicates some sort of request failure. For example, we had this convention of naming our actions on Divar’s website: FETCH_SOMETHING, FETCH_SOMETHING_SUCCESS, and FETCH_SOMETHING_FAILED. This convention would help us show loading UI and also, later on, handle failed requests. How?

I wrote a middleware that would check for the ending of the action’s name. If it ended with _FAILED I knew that something is wrong and I could provide the retry option to the user. Here is a sample code:

const retryMiddleware = store => next => action => {
if (!action.type.endsWith('_FAILED')) return next(action)

const newAction = {...action, type: action.type.replace('_FAILED', '')}
const retry = () => store.dispatch(newAction)

store.dispatch(showApiErrorFeedback(retry))

return next(action);
}

And then, there’s a component that would render the feedback if there were any errors with a simple retry button like this:

<Button onClick={store.errorFeedback.retry}>Retry</Button>

2. ReactQuery

ReactQuery would need another approach. After defining some kind of ErrorContext at the top of our component hierarchy, we can use it this way:

function useSomethingApi() {
const api = useQuery('something', fetchSomething);

useHandleApiError('Unable to fetch something!', api);

return api;
}

function useHandleApiError(message, api) {
const { addError } = useErrorContext();
if (!api) return;

const { error, refetch } = api;
if (!error) return;

addError(message, refetch);
}

ErrorContext can then store these errors inside a state, and show the feedback to the user.

In Snapp Express, we used to have a legacy app that was based on redux-saga and then we added ReactQuery. So we used a combination of both ways to handle each failed request.

No network connection

If you are registering a service worker on the browser (and there are plenty of reasons why you should), you can then check if the browser is connected to the internet every time the user opens your website and if he/she was not connected, show custom feedback to the user to help him/her identify the problem easier. There are some great articles about the implementation. I just wanted to point out that it would be beneficial for your business if you did so.

Crash free rate

I don’t know about other logging services, but Sentry has a very useful feature that would separate the rate of application crashes based on the version. We used to have a unique version number for each production build and we used this version for a few purposes. One of them was to tag each error on Sentry with the version. This way, Sentry could tell us how many users are facing errors for each different build and we could also track the raise or fall of this ratio with each release. And we could fix those issues before anyone else encountered them (at least anyone else in the company!)

Summary

With this legacy app that I mentioned earlier, things were horrible at first. The crash-free rate used to be around 90 percent! Which practically meant that the app is not useful. But we worked our way up to 99.9998%. Which kept pretty much everyone satisfied.

The most important thing you need to keep in mind about errors is, they are not usual or even high-priority tasks. They are the kind of tasks that you shouldn’t be calling a day until they are fixed.

--

--