Graceful error handling is an essential element of well designed software. This is true of front-end JavaScript user interfaces, and ReactJS provides specialized error handling for dealing with render-time errors. This article offers an overview for dealing with errors in ReactJS applications.
[ Also on InfoWorld: How to use React functional components ]
We can divide errors broadly into two types, and error handling into two aspects.
The two error types:
- JavaScript errors
- Render errors
JavaScript errors are those which occur in the code and can be handled with standard try/catch blocks, while render errors occur in the view templates and are handled by React error boundaries.
The two aspects of error handling:
- Displaying information to the user
- Providing information to the developer
In general, you want to show only the minimum amount of error information to users, and you want to reveal the maximum amount of information to developers, both at development time and at other times like test and production.
React error boundaries
The most distinctive and React-specific type of error handling is what is known as error boundaries. This feature was introduced in React 16 and allows you to define components that act as error-catching mechanisms for the component tree below them.
The core idea is to build a widget that conditionally renders a view depending upon its error state. React provides two lifecycle methods that a component can implement to determine if a rendering error has occurred in its child tree and respond accordingly.
These two methods are componentDidCatch()
and static getDerivedStateFromError()
. In both cases, the chief purpose is to update the component state so it can respond to errors arriving from the React engine.
getDerivedStateFromError
Because getDerivedFromError()
is static, it does not have access to the component state. Its only purpose is to receive an error object, and then return an object that will be added to the component state. For example, see Listing 1.
Listing 1. getDerivedStateFromError()
static getDerivedStateFromError(error) {
return { isError: true };
}
Listing 1 returns an object with an error flag that can then be used by the component in its rendering.
componentDidCatch
componentDidCatch()
is a normal method and can update the component state, as well as take actions (like making a service call to an error-reporting back end). Listing 2 has a look at using this method.
Listing 2. componentDidCatch
componentDidCatch(error, errorInfo) {
errorService.report(errorInfo);
this.setState({ error: error, errorInfo: errorInfo })
}
In Listing 2, again the primary function makes sure the component state understands an error has occurred and passes along the info about that error.
Rendering based on error
Let’s have a look at rendering for our error handling component, as seen in Listing 3.
Listing 3. ErrorBoundary rendering
render() {
if (this.state.error && this.state.errorInfo) {
return (
<div>
<p>Caught an Error: {this.state.error.toString()}</p>
<div>
{this.state.errorInfo.componentStack}
</div>
</div>
);
} else {
return this.props.children;
}
}
From Listing 3 you can see that the default action of the component is to just render its children. That is, it’s a simple pass-through component. If an error state is found (as in Listing 1 or Listing 2), then the alternative view is rendered.
Using the ErrorBoundary component
You've now seen the essential elements of an error handler component in React. Using the component is very simple, as seen in Listing 4.
Listing 4. ErrorBoundary component example
<Parent>
<ErrorBoundary>
<Child><Child/>
</ErrorBoundary>
</Parent>
In Listing 4, any rendering errors in <Child>
will trigger the alternate rendering of the error handling <ErrorBoundary>
component. You can see that error boundary components act as a kind of declarative try/catch block in the view.
JavaScript errors
JavaScript errors are handled by wrapping code in try/catch blocks. This is well understood and works great, but there are a few comments to make in the context of a React UI.
First, it’s important to note that these errors do not propagate to error boundary components. It’s possible to bubble errors manually via normal React functional properties, and it would be possible thereby to tie the error handling into the conditional rendering found in your error boundaries.
Another point to make is that in dealing with network or server-side errors arising from API calls, these should be handled with the built-in error codes, as in Listing 5.
Listing 5. Using built-in error codes
let response = await fetch(process.env.REACT_APP_API +
'/api/describe?_id='+this.state.projectId, {
headers: { "Authorization": this.props.userData.userData.jwt },
method: 'GET',
});
if (response.ok){
let json = await response.json();
console.info(json);
this.setState({ "project": json});
} else {
console.error("Problem: " + response);
}
Finally, in connection with both render and JavaScript errors, remember that it can be useful to log errors via a remote error reporting API. This is handled by class-based components that implement the componentDidCatch
method.
Summing up
You can think of error boundaries as declarative error catch blocks for your view markup. As of React 16, if your rendering in a component causes an error, the entire component tree will not render. Otherwise, the error will bubble up until the first error handling component is encountered. Prior to React 16, errors would leave the component tree partially rendered.
Error boundary components must be class-based, although there are plans to add hook support for the lifecycle.
As we’ve seen, the basic idea is that you create a component which conditionally renders based on the error state. There are two means for accomplishing this: the componentDidCatch()
method or the static getDerivedStateFromError()
method.
The code examples in this article refer to this CodePen (derived from the example found in the React docs). You might also find it useful to check out this CodePen example of error boundaries in React 16.