OpenReplay
Navigate back to the homepage
BLOG
Browse Repo
Back

Catching Errors in React with Error Boundaries

Kristofer Selbekk
August 8th, 2021 · 4 min read

Even the most flawless applications have runtime errors from time to time. The network might time out, some backend service might go down, or your users might provide you with some input that just doesn’t compute. Or – you know – bugs. But what’s the best way to handle errors to ensure your app is resilient, continues to be responsive and provide the best possible user experience?

This article is going to introduce you to the concept of error boundaries in React. We’ll look at what challenges they try to solve, what shortcomings they have, and how to implement them. Lastly, we’ll look at a small abstraction layer that makes error boundaries even better!

What is an error boundary?

Error boundaries is the React way to handle errors in your application. It lets you react and recover from runtime errors as well as providing a fallback user interface if applicable.

The idea behind error boundaries is that you can wrap any part of your application in a special component – a so-called error boundary – and if that part of your application experiences an uncaught error, it will be contained within that component. You can then show an error, report it to your favorite error reporting service, and try to recover if possible.

Error boundaries were introduced in React 16, and was one of the first features that came out of the React team’s Fiber rewrite effort. They are the only component you still need to write as a class component (so no hooks as of yet!), but should definitely be a part of any modern React application.

Note that even though you can create several error boundaries throughout your application, many applications tend to opt for a single one at the root level. You can go super-granular if you wish, but my experience tells me that a root level one often suffices.

My first error boundary

An error boundary is a regular class component that implements one (or both) of the following methods:

static getDerivedStateFromError(error)

This method returns a new state, based on the error caught. Typically, you will flip a state flag that tells the error boundary whether or not to provide a fallback user interface.

componentDidCatch(error, errorInfo)

This method is called whenever an error occurs. You can log the error (and any extra information) to your favorite error reporting service, try to recover from the error, and whatever else you need to do.

To show how this is implemented, let’s create one step by step. First - let’s create a regular class component.

1class ErrorBoundary extends React.Component {
2 render() {
3 return this.props.children;
4 }
5}

This component doesn’t do much at all - it just renders its children. Let’s log the error to an error service!

1class ErrorBoundary extends React.Component {
2 componentDidCatch(error, errorInfo) {
3 errorService.log({ error, errorInfo });
4 }
5 render() {
6 return this.props.children;
7 }
8}

Now, whenever an error occurs for our users, we’re notified through our error reporting service. We’ll receive the error itself, as well as the full component stack where the error occurred. This is going to greatly simplify our bug fixing work later on!

However, we’re still breaking the application! That’s not great. Let’s provide a fallback “oops” UI. In order to do that, we need to track whether or not we’re in an erroneous state - and that’s where the static getDerivedStateFromError method comes in!

1class ErrorBoundary extends React.Component {
2 state = { hasError: false };
3 static getDerivedStateFromError(error) {
4 return { hasError: true };
5 }
6 componentDidCatch(error, errorInfo) {
7 errorService.log({ error, errorInfo });
8 }
9 render() {
10 if (this.state.hasError) {
11 return <h1>Oops, we done goofed up</h1>;
12 }
13 return this.props.children;
14 }
15}

And now we have a basic, yet functional error boundary!

Start using it

Now, let’s start using it. Simply wrap your root app component in your new ErrorBoundary component!

1ReactDOM.render(
2 <ErrorBoundary>
3 <App />
4 </ErrorBoundary>,
5 document.getElementById('root')
6)

Note that you might want to place your error boundaries so that it shows some basic layout (header, footer etc) as well.

Add a reset functionality!

Sometimes, errors like this happen when the UI gets in some funky state. Whenever an error occurs, the entire sub-tree of the error boundary is unmounted, which in turn will reset any internal state. Providing the user with a “Want to try again” button, which will try to remount the sub-tree with fresh state could be a good idea at times! Let’s do that next.

1class ErrorBoundary extends React.Component {
2 state = { hasError: false };
3 static getDerivedStateFromError(error) {
4 return { hasError: true };
5 }
6 componentDidCatch(error, errorInfo) {
7 errorService.log({ error, errorInfo });
8 }
9 render() {
10 if (this.state.hasError) {
11 return (
12 <div>
13 <h1>Oops, we done goofed up</h1>
14 <button type="button" onClick={() => this.setState({ hasError: false })}>
15 Try again?
16 </button>
17 </div>
18 );
19 }
20 return this.props.children;
21 }
22}

Of course, this might not be a good idea for your application. Take your own needs and users into consideration when implementing features like this.

Open Source Session Replay

Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder. OpenReplay is the only open-source alternative currently available.

OpenReplay

Happy debugging, for modern frontend teams - Start monitoring your web app for free.

Limitations

Error boundaries are great for what they do - catch runtime errors you didn’t expect during rendering. However, there are a few types of errors that aren’t caught, and that you need to deal with in a different way. These include:

  • errors in event handlers (when you click a button for instance)
  • errors in asynchronous callbacks (setTimeout for instance)
  • errors that happen in the error boundary component itself
  • errors that occur during server side rendering

These limitations might sound severe, but most of the time they can be worked around by using try-catch and a similar hasError state.

1function SignUpButton(props) {
2 const [hasError, setError] = React.useState(false);
3 const handleClick = async () => {
4 try {
5 await api.signUp();
6 } catch(error) {
7 errorService.log({ error })
8 setError(true);
9 }
10 }
11 if (hasError) {
12 return <p>Sign up failed!</p>;
13 }
14 return <button onClick={handleClick}>Sign up</button>;
15}

This works well enough, even if you do have to duplicate a few lines of code.

Creating a better error boundary

Error boundaries are good by default, but it would be great to reuse their error handling logic in event handlers and asynchronous places as well. It’s easy enough to implement through the context API!

To give our error boundary super powers, let’s implement a function for triggering errors manually.

1class ErrorBoundary extends React.Component {
2 state = { hasError: false };
3 static getDerivedStateFromError(error) {
4 return { hasError: true };
5 }
6 componentDidCatch(error, errorInfo) {
7 errorService.log({ error, errorInfo });
8 }
9 triggerError = ({ error, errorInfo }) => {
10 errorService.log({ error, errorInfo });
11 this.setState({ hasError: true });
12 }
13 resetError = () => this.setState({ hasError: false });
14 render() {
15 if (this.state.hasError) {
16 return <h1>Oops, we done goofed up</h1>;
17 }
18 return this.props.children;
19 }
20}

Next, let’s create a context and pass our new function into it:

1const ErrorBoundaryContext = React.createContext(() => {});

We can then create a custom hook to retrieve the error triggering function from any child component:

1const useErrorHandling = () => {
2 return React.useContext(ErrorBoundaryContext)
3}

Next, let’s wrap our error boundary in this context:

1class ErrorBoundary extends React.Component {
2 state = { hasError: false };
3 static getDerivedStateFromError(error) {
4 return { hasError: true };
5 }
6 componentDidCatch(error, errorInfo) {
7 errorService.log({ error, errorInfo });
8 }
9 triggerError = ({ error, errorInfo }) => {
10 errorService.log({ error, errorInfo });
11 this.setState({ hasError: true });
12 }
13 resetError = () => this.setState({ hasError: false });
14 render() {
15 return (
16 <ErrorBoundaryContext.Provider value={this.triggerError}>
17 {this.state.hasError
18 ? <h1>Oops, we done goofed up</h1>
19 : this.props.children
20 }
21 </ErrorBoundaryContext.Provider>
22 );
23 }
24}

Now we can trigger errors from our event handlers as well!

1function SignUpButton(props) {
2 const { triggerError } = useErrorHandling();
3 const handleClick = async () => {
4 try {
5 await api.signUp();
6 } catch(error) {
7 triggerError(error);
8 }
9 }
10 return <button onClick={handleClick}>Sign up</button>;
11}

Now we don’t have to think about error reporting or creating a fallback UI for every click handler we implement - it’s all in the error boundary component.

Using react-error-boundary

Writing your own error boundary logic like we did above is fine, and should handle most use cases for you. However, this is a solved problem. React Core team member Brian Vaughn (and later, the very talented React teacher Kent C. Dodds) have spent a bit of time creating a [react-error-boundary](https://www.npmjs.com/package/react-error-boundary) npm package that gives you pretty much the same thing as above.

The API is a bit different, so you can pass in custom fallback components and reset logic instead of writing your own, but it’s used in a very similar way. Here’s an example:

1ReactDOM.render(
2 <ErrorBoundary
3 FallbackComponent={MyFallbackComponent}
4 onError={(error, errorInfo) => errorService.log({ error, errorInfo })}
5 >
6 <App />
7 </ErrorBoundary>,
8 document.getElementById('root')
9)

You can also have a look at how it’s implemented - it uses a different approach to triggering errors from hooks, but otherwise it works pretty much the same way.

Summary

Handling errors and unexpected events is crucial for any quality application. It’s extremely important to provide a great user experience, even when things doesn’t go as planned.

Error boundaries is a great way to make your application fail gracefully, and even contain errors to parts of your application while the rest continues to work! Write your own, or adopt the react-error-boundary library to do it for you. No matter what you choose, your users will thank you!

More articles from OpenReplay Blog

How to Safely Render Markdown From a React Component

Step-by-step guide on how to safely render Markdown within your React Component using react-markdown

July 30th, 2021 · 8 min read

The Complete Guide to Localizing your App with JavaScript's Internationalization API

Internationalizaton is Easy or so they say. Learn how to use the internationalization API from JavaScript

July 30th, 2021 · 9 min read
© 2021 OpenReplay Blog
Link to $https://twitter.com/OpenReplayHQLink to $https://github.com/openreplay/openreplayLink to $https://www.linkedin.com/company/18257552