X
Popular Searches

How to Use React’s Error Boundaries to Catch Crashes

React logo on a dark background

React error boundaries let you catch JavaScript errors that occur in child components. Any unhandled error originating below the boundary’s tree position will be caught, preventing a crash occurring.

You can display your own fallback UI after an error boundary traps an error. This lets you gracefully communicate the problem to the user. They’ll be able to keep using the rest of your interface without suffering a completely crashed tab.

Creating Error Boundaries

Any React class component can become an error boundary. You just need to set either of the following lifecycle methods:

  • componentDidCatch(error) – This instance method will be called whenever the component catches an error. You can use this to report the error to an analytics or monitoring service.
  • static getDerivedStateFromError(error) – This static method can be used to update your component’s state after an error occurs. This is how you display a fallback UI.

Here’s what the two methods look like in use:

componentDidCatch()

class MyComponent extends React.Component {
 
    componentDidCatch(error) {
        // use custom reporting framework
        logErrorToAnalytics(error);
        this.props.onError(error);
    }
 
}

Using componentDidCatch(), your component can report the error in whichever way it sees fit. As it’s an instance method, you may also pass it up the component tree via props.

getDerivedStateFromError(error)

class MyComponent extends React.Component {
 
    state = {error: null};
 
    render() {
        return <h1>{!this.state.error ? "Hello" : "Error"}</h1>;
    }
 
    static getDerivedStateFromError(error) {
        return {error};
    }
 
}

getDerivedStateFromError() also receives the JavaScript error object. It must return an object describing the state transformation to apply to your component.

Advertisement

React will pass the returned object to setState(). In this example, the value of the error key in the component’s state will get set to the caught error object. This will result in the rendered output changing to Error instead of the standard Hello text.

Which Method to Use?

If the two boundary methods seem similar, it’s because they are! Technically, you can define either or both of these methods and still have the same results – componentDidCatch() could call setState() to update your component’s state, and getDerivedStateFromError() could call an external monitoring service to report errors it captures.

The difference lies in the phase in which the error is caught. componentDidCatch() captures errors in the commit phase, after React has updated the DOM. getDerivedStateFromError() will be called during the render phase, before React updates the browser’s DOM.

This timing subtlety explains why getDerivedStateFromError() is generally used to switch to the fallback UI. When a serious error occurs, the act of updating the DOM might provoke further errors if your app’s been left in an inconsistent state. Updating the state prior to the DOM update occurring ensures the fallback UI renders immediately.

Locating Your Error Boundaries

You’re free to use error boundaries wherever you see fit. It’s good practice to use multiple error boundaries. Add an error boundary for each major layer of your UI. This lets you isolate errors in your page content from the application shell, so a crash in a route doesn’t take out your navigation bar.

Here’s a simple component hierarchy:

export const () => (
    <App>
        <Header />
        <Router />
        <Footer />
    </App>
);

In this application, the App component is a simple wrapper managing top-level state. Header renders a navigation bar and Footer displays the bottom bar. The main page content – where crashes are most likely to occur – is loaded dynamically by Router, based on the current URL.

Advertisement

By default, a crash within the Router children knocks out the entire site. By placing an error boundary around Router, errors occurring within the component can be gracefully handled. The header and footer remain usable while the main page content is replaced with a fallback message.

The app needs at least one more error boundary. Wrapping the children of App ensures errors arising within the header or footer can be caught. In this situation, it might be acceptable to replace the entire UI with a full-page error message.

Here’s the refactored component structure:

class ErrorBoundary extends React.Component {
 
    state = {error: null};
 
    render() {
        if (!this.state.error) return this.props.children;
        else return <h1>Error!</h1>;
    }
 
    static getDerivedStateFromError(error) {
        return {error};
    }
 
}
 
export const () => (
    <App>
        <ErrorBoundary>
            <Header>
            <ErrorBoundary>
                <Router />
            </ErrorBoundary>
            <Footer />
        </ErrorBoundary>
    </App>
);

We’ve abstracted the error boundary logic into a reusable component. We can now wrap ErrorBoundary around any components which should be isolated from their parents. Remember you don’t have to create an error boundary component – for simple applications, or a specific UI component, you can add the lifecycle hooks directly into a component class.

Limitations of Error Boundaries

Error boundaries have some important limitations you should be aware of. They’re capable of catching most unhandled JavaScript errors but some will go undetected.

Error boundaries won’t intercept errors that occur in event handler methods. Event handler code doesn’t affect React’s rendering process so the framework can still render your components. As event handler errors won’t result in corrupted UI or component unmounts, React doesn’t try to intercept them.

If you need to respond to errors in your event handlers, you must use a regular try/catch block. Perform a state update in the catch statement to switch your UI into an error state.

class MyComponent extends React.Component {
 
    state = {error: null};
 
    handleClick = () => {
        try {
            doSomething();
        }
        catch (error) {
            this.setState({error});
        }
    }
 
    render() {
        if (this.state.error) return <p>Error!</p>;
        else return <button onClick={this.handleClick}>Submit</button>
    }
 
}
Advertisement

Aside from event handlers, error boundaries can’t detect errors that occur in asynchronous code. If you’re using Promises, async/await, or setTimeout(), you should make sure you’ve using try/catch/Promise.catch() blocks to catch any errors that might occur.

A common misunderstanding around error boundaries concerns the tree they monitor. They can only catch errors that occur deeper in the tree. Error boundaries won’t catch errors thrown by the boundary component itself.

export default () => (
    <App>                   // Errors won't be caught
        <ErrorBoundary>     // Errors won't be caught
            <Router />      // Errors thrown here will be caught
        </ErrorBoundary>
    </App>
);

Each error boundary must wrap around the components that might throw an error.

Finally, only class-based components can be error boundaries. There’s currently no mechanism to let a functional component become an error boundary. If you’re working in a functional codebase, you should create a reusable error boundary component like that shown above. You can then wrap your components with it each time you need an error boundary.

Conclusion

Error boundaries bring JavaScript’s try/catch to React’s declarative rendering model. They let you isolate parts of your site’s UI, so a crash in one component won’t affect its siblings.

You should evaluate your app’s UI to identify the critical sections. Place error boundaries strategically to stop an unhandled error from unmounting your entire component tree. Users are much more likely to accept a fully stylised “something’s gone wrong” than a white screen that needs refreshing.

James Walker James Walker
James Walker is a contributor to CloudSavvy IT. He is the founder of Heron Web, a UK-based digital agency providing bespoke software development services to SMEs. He has experience managing complete end-to-end web development workflows, using technologies including Linux, GitLab, Docker, and Kubernetes. Read Full Bio »

The above article may contain affiliate links, which help support CloudSavvy IT.