ReactJS error boundary + Sentry


Understanding error boundary feature

Thinking and building in React involves approaching application design in chunks, or components. Every part of your application that performs an action should be treated as a component.

Before React 16, JavaScript errors inside components used to corrupt React’s internal state and cause it to emit cryptic errors on next renders. These errors were always caused by an earlier error in the application code, but React did not provide a way to handle them gracefully in components, and could not recover from them.

These kinds of render-based errors cannot be caught since React components are declarative. Hence, you can’t just throw in a try…catch block inside a component. A JavaScript error in a part of the UI shouldn’t break the whole app. To solve this problem Luckily, the Error Boundary feature was introduced in React 16. According to React Documentation,

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.

Basically, error boundaries are React Components. Error boundaries catch JavaScript errors anywhere in their child component tree. By wrapping the whole tree up in an ErrorBoundaryComponent, we can catch any JavaScript errors that occur in its child components. An error boundary can’t catch an error within itself.

Error boundaries do not catch errors for:

  • Event handlers
  • Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
  • Server side rendering
  • Errors thrown in the error boundary itself (rather than its children)

Now let's learn how to use error boundary

A class component becomes an error boundary if it defines either (or both) of the lifecycle methods static getDerivedStateFromError() or componentDidCatch(). Use static getDerivedStateFromError() to render a fallback UI after an error has been thrown. Use componentDidCatch() to log error information.

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

Then you can use it as a regular component:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

Let's implement this in a demo application

Step 1: Create a react project, run:

npx create-react-app my-app
cd my-app
npm start

Step 2: Create a buggy component

In this component we are creating a simple click counter, when the click limit reaches count 5, it throws an error.

import React from 'react';

class BuggyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {counter: 0};
        this.handleClick = this.handleClick.bind(this);
    }

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

    render() {
        if (this.state.counter === 5) {
            // Simulate a JS error
            throw new Error('I crashed!');
        }
        return <h1 onClick={this.handleClick}>Click counter - {this.state.counter}</h1>;
    }
}

export default BuggyComponent;

Step 3: Create an error boundary component

import React from 'react';

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = {error: null, errorInfo: null, eventId: null};
    }

    componentDidCatch(error, errorInfo) {
        // Catch errors in any components below and re-render with error message
        this.setState({
            error: error,
            errorInfo: errorInfo,
        })

        // You can also log error messages to an error reporting service here
    }

    render() {
        if (this.state.errorInfo) {
            // Error path
            return (
                <div>
                    <h2>Something went wrong.</h2>
                    <details style={{whiteSpace: 'pre-wrap'}}>
                        {this.state.error && this.state.error.toString()}
                        <br/>
                        {this.state.errorInfo.componentStack}
                    </details>
                </div>
            );
        }
        // Normally, just render children
        return this.props.children;
    }
}

export default ErrorBoundary;

Step 4: Use error boundary in App.js

Open the app.js file of the project and add BuggyComponent inside it. In the example we are going to use BuggyComponent instances, one with Error Boundary as its parent component and one without Error Boundary.

import React from 'react';
import logo from './logo.svg';
import './App.css';
import ErrorBoundary from "./ErrorBoundary";
import BuggyComponent from "./BuggyComponent";

function App() {
    return (
        <div className="App">
            <header className="App-header">
                <img src={logo} className="App-logo" alt="logo"/>
                <p>
                    Edit <code>src/App.js</code> and save to reload.
                </p>
                <a
                    className="App-link"
                    href="https://reactjs.org"
                    target="_blank"
                    rel="noopener noreferrer"
                >
                    Learn React
                </a>
                <p>Buddy counter with error boundary</p>
                <ErrorBoundary>
                    <BuggyComponent/>
                </ErrorBoundary>
                <p>Buddy counter without error boundary</p>
                <BuggyComponent/>
            </header>
        </div>
    );
}

export default App;

Here's the demo link, if you want to check how it will work.

Demo

In the demo, you can see if you click on click counter which is not using error boundary throws error it breaks the entire application, whereas in case of error boundary it displays a fallback UI and without impacting the rest of the components.

Bonus Step: Adding Sentry

Too many applications take a casual approach to error detection, handling, and reporting because developers view it as either drudgery or unimportant. But it is really important if you want your product to work flawlessly.

What is Sentry and why we are using it?

Sentry is Open-source error tracking that helps developers to monitor, fix crashes in real time. Don’t forget about boosting the efficiency, improving user experience. Sentry has support for JavaScript, Node, Python, PHP, Ruby, Java and other programming languages. You can get more information here.

To use Sentry with your React application, you will need to use @sentry/browser (Sentry’s browser JavaScript SDK).

Add the @sentry/browser to your project:

npm install --save @sentry/browser

Connecting the SDK to Sentry

After you’ve completed setting up a project in Sentry, Sentry will give you a value which we call a DSN or Data Source Name. We have to use that DSN value as an environment variable. You should init the Sentry browser SDK as soon as possible during your application load up, before initializing React. In our case it would be index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import * as Sentry from '@sentry/browser';

Sentry.init({
    dsn: process.env.REACT_APP_SENTRY_DSN,
    environment: process.env.NODE_ENV
});

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Now let's send all the errors that our error boundary catches to Sentry using Sentry.captureException. This is also a great opportunity to collect user feedback by using Sentry.showReportDialog.

import React from "react";
import * as Sentry from '@sentry/browser';

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = {error: null, errorInfo: null, eventId: null};
    }

    componentDidCatch(error, errorInfo) {
        // Catch errors in any components below and re-render with error message
        this.setState({
            error: error,
            errorInfo: errorInfo,
        })
        Sentry.withScope(scope => {
            scope.setExtras(errorInfo);
            const eventId = Sentry.captureException(error);
            this.setState({eventId})
        });
        // You can also log error messages to an error reporting service here
    }

    render() {
        if (this.state.errorInfo) {
            // Error path
            return (
                <div>
                    <h2>Something went wrong.</h2>
                    <details style={{whiteSpace: 'pre-wrap'}}>
                        {this.state.error && this.state.error.toString()}
                        <br/>
                        {this.state.errorInfo.componentStack}
                    </details>
                    <button onClick={() => Sentry.showReportDialog({eventId: this.state.eventId})}>Report feedback</button>
                </div>
            );
        }
        // Normally, just render children
        return this.props.children;
    }
}

export default ErrorBoundary;

You can find all the source code in this repository Github.