Tutorial

How To Use Error Boundaries in React

React

Introduction

Error Boundaries were introduced in React v16 as a way to catch tricky errors that occur during the render phase. In the past, this would have caused the app to unmount completely, and the user would just see a blank web page, which is not ideal!

In this article, you’ll learn about Error Boundaries via code snippets and interactive demos.

Prerequisites

To follow along with this article, you will need:

This tutorial was verified with Node v16.4.2, npm v7.19.1, and react v17.0.2.

Encountering Errors without Error Boundaries

We will inevitably encounter unexpected errors in our apps during development. You could be trying to access a deeply-nested property on an object that does not exist, or sometimes it is not in your control (like a failed HTTP request to a third-party API).

In the demo below, we will simulate an error to see what normally happens without an Error Boundary.

BuggyCounter.js
import React from 'react';

class BuggyCounter extends React.Component {
  state = {
    counter: 0,
  };

  handleClick = () => {
    this.setState({
      counter: this.state.counter + 1,
    });
  };

  render() {
    if (this.state.counter === 5) {
      // Simulate an error!
      throw new Error('Simulated error.');
    }
    return (
      <div>
        <h1>{this.state.counter}</h1>
        <button onClick={this.handleClick}>+</button>
      </div>
    );
  }
}

export default BuggyCounter;

Visit the live code example alligatorio-react-error-boundaries-1 by @wle8300 on CodePen.

Click the + (increment) button and observe how it fails at 5.

Output
Uncaught Error: Simulated error.

When the app encounters an error, the component completely unmounts itself and the user is left with a blank HTML page. This can leave users feeling confused and they will not know what to do next.

Error Boundaries provide a way to gracefully handle these errors!

Encountering Errors with Error Boundaries

What are Error Boundaries exactly? Contrary to what you may think, it’s not a new component or JavaScript library. It’s more like a strategy for handling errors in React components.

Specifically, it is the usage of two methods that are available in React components:

MyErrorBoundaryExample.js
import React from 'react';

class MyErrorBoundaryExample extends React.Component {
  state = {
    error: null,
  };

  static getDerivedStateFromError(error) {
    // Update state so next render shows fallback UI.
    return { error: error };
  }

  componentDidCatch(error, info) {
    // Log the error to an error reporting service
    logErrorToExampleService(error, info);
  }

  render() {
    if (this.state.error) {
      // You can render any custom fallback UI
      return <p>Something broke</p>;
    }
    return this.props.children;
  }
}

export default MyErrorBoundaryExample;
  • static getDerivedStateFromError is a lifecycle method that allows the Error Boundary a chance to update the state and thus triggering a last render(). In the above code snippet, the state is used to reveal a human-friendly error message instead of the broken component (e.g., this.props.children).
  • componentDidCatch is a lifecycle method designed for triggering side-effects (e.g., logging the error to tools like Crashlytics). You can access info.componentStack to get a developer-friendly stack trace that will be useful for triaging the bug.

Any React Component is considered an Error Boundary when it employs at least one of these lifecycle methods.

Good practices suggest that you will want to create a component that is purpose-built as an Error Boundary instead of mixing error-handling logic into your generic components.

Let’s slightly modify <MyErrorBoundary>, and then wrap it around <BuggyComponent> so it will catch the error!

MyErrorBoundary.js
import React from 'react';

class MyErrorBoundary extends React.Component {
  state = {
    errorMessage: '',
  };

  static getDerivedStateFromError(error) {
    return { errorMessage: error.toString() };
  }

  componentDidCatch(error, info) {
    this.logErrorToServices(error.toString(), info.componentStack);
  }

  // A fake logging service.
  logErrorToServices = console.log;

  render() {
    if (this.state.errorMessage) {
      return <p>{this.state.errorMessage}</p>;
    }
    return this.props.children;
  }
}

export default MyErrorBoundary;

And App.js:

App.js
import React from 'react';
import BuggyCounter from './BuggyCounter';
import MyErrorBoudnary from './MyErrorBoundary';

class App extends React.Component {
  refreshPage = () => {
    history.go(0);
  };

  render() {
    return (
      <div>
        <MyErrorBoundary>
          <BuggyCounter />
        </MyErrorBoundary>
        <hr />
        <button onClick={this.refreshPage}>Refresh Page</button>
      </div>
    );
  }
}

export default App;

BuggyCounter.js remains the same.

Visit the live code example alligatorio-react-error-boundaries-2 by @wle8300 on CodePen.

Try clicking the + (increment) button again. Once it reaches 5, it will crash gracefully. Additionally, you can open your console to view a stack trace!

Instead of completely crashing, we can use Error Boundaries to substitute a fallback UI. This provides visual feedback to the user that something broke while allowing them to continue interacting with our app.

They can choose to navigate away or even reach out to customer service to help resolve their situation! It is a great way to redeem an otherwise unfortunate user experience.

Comparing Error Boundaries and Try...Catch

Error Boundaries actually aren’t in direct competition with try...catch statements. Error Boundaries are only designed for intercepting errors that originate from 3 places in a React component:

  • During render phase
  • In a lifecycle method
  • In the constructor

Basically… the React-y parts of a component.

As a counterpoint, these are the places where Error Boundaries will not be able to catch an error:

  • Event handlers (e.g., onClick, onChange, etc.)
  • setTimeout or requestAnimationFramecallbacks
  • Server-side rendering (SSR)
  • And errors caused by the error boundary itself (rather than its children)

So Error Boundaries don’t really impact how you use try...catch. They’re both needed as a robust strategy for handling errors in React.

Conclusion

In this article, you learned about Error Boundaries.

Note: Error Boundaries are only available in Class-based React Components. At the time of this writing, there is currently not a way to implement it using React Hooks.

Now that Error Boundaries are available since React version 16, it’s generally advisable to use at least one Error Boundary at the root of your app (e.g., the App.js file). This will prevent users from seeing a blank HTML page and perhaps see a nice fallback UI instead.

Going forward, you can employ several different kinds of Error Boundaries that employ different fallback UIs or those that only log errors to a third-party service.

Check out the official React docs for more info!

Creative Commons License