Graceful error handling is an essential aspect of well-designed software. It’s also tricky. This article offers an overview of error handling in React applications and how to use React error boundaries to handle render-time errors.React error typesWe can divide React application errors broadly into two types, and error handling into two aspects.The two React error types:
JavaScript errors: These are conventional JavaScript errors that occur in the code portion of a component.
Render errors: These are errors raised by the rendering engine, emerging from the markup.
Note that the nature of JavaScript UI makes for tricky error handling. Aside from typical runtime errors, there are errors that spring from the “drawing” of the screen components. We are distinguishing these two types of errors here as “JavaScript errors” and “Render errors.”JavaScript errors 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. We can think of error boundaries as try/catch blocks expressed in the template markup.There are two aspects of error handling in both cases:
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 testing and production. A good rule of thumb for error handling is to “fail soft” for users and “fail hard” for developers.React error boundariesThe 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. We’ll see an example shortly, but a render error is one that only becomes apparent when the React engine is interpreting the markup. This can happen anywhere in the hierarchy of components that go into an interface.The core idea is to build a widget that conditionally renders a view depending on 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 getDerivedStateFromError(), which is static. In both cases, the chief purpose is to update the component state so it can respond to errors arriving from the React engine.componentDidCatch()The componentDidCatch() method is normal and can update the component state, as well as take actions such as making a service call to an error-reporting back end. Listing 1 shows us using this method. Listing 1. componentDidCatch
componentDidCatch(error, errorInfo) {
errorService.report(errorInfo);
this.setState({ error: error, errorInfo: errorInfo })
}
In Listing 1, the primary function ensures the component state understands an error has occurred and passes along the information about that error. Note that this componentDidCatch() has access to the runtime.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 2.Listing 2. getDerivedStateFromError()
static getDerivedStateFromError(error) {
return { isError: true };
}
Listing 2 returns an object with an error flag that can then be used by the component in its rendering. We could handle more elaborate needs by constructing more complex error objects.Rendering based on errorNow, 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 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. While the default behavior is to render the interface, an error state invokes an alternative path, something like a catch block.Using the ErrorBoundary componentYou’ve now seen the essential elements of an error-handling component in React. Using the component is very simple, as shown 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. Any children of <Child> will also bubble up to <ErrorBoundary> unless they’re caught by some other error boundary along the way—also analogous to try/catch behavior.JavaScript errorsJavaScript errors are handled by wrapping code in try/catch blocks, as in standard JavaScript. 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.Network errorsNetwork or server-side errors arising from API calls should be handled using built-in error codes, as shown 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);
throw new Error(“Problem fetching user info”, response);
}
The point of Listing 5 is to use the standard HTTP status codes to determine the error state of the network request. (Sometimes it’s tempting to use a custom “status” field.) In this case, the error is detected by checking response.ok, and then if there’s an error, we raise a new error with throw. In this case, we’d expect a catch handler to deal with the situation.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.Error handling in actionThe code examples in this article refer to CodePen, which is derived from the example found in the React docs. This pen gives you four error conditions, each represented by a colored div with a link. The link will trigger an error. The bottom row gives you JavaScript errors, the top row gives you render errors. The first column is not wrapped with an error boundary, the second column is wrapped. So this gives you a look at the code and behavior of several possible error states:
The green box will throw a render-time error when clicked, and it is caught with a boundary, displaying an error message.
The red box will throw a render error without a boundary, and as you can see, the app will crash and stop rendering.
The blue and purple boxes both handle errors the same way, since the error boundary does not capture the JavaScript error.
These examples are worth examining as they give you all the working elements of error boundaries in a bit-sized package.You might also find it useful to check out this CodePen example of error boundaries in React 16.ConclusionYou 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. Before 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 that conditionally renders based on the error state. There are two ways to accomplish this: the componentDidCatch() method or the static getDerivedStateFromError() method.
Copyright © 2024 IDG Communications, Inc.
Source link