Containing Component Failures

React's component model encourages breaking an interface into small, focused units. That same granularity, however, raises a practical question: why should an error in one small component take down the entire application? Before React 16, that is exactly what happened, often leaving developers with cryptic console output and no clear path to a fix.

Error boundaries were introduced in React 16 as a way to contain those failures. An error boundary is a class component that wraps other components and intercepts errors thrown during rendering. If a child component throws, the boundary catches the error before it propagates up the tree and unmounts the whole UI.

The value of this approach is straightforward. A failure in a sidebar, for instance, should not blank out the header or the main content area. By placing a boundary around a specific region, you keep the rest of the interface interactive and usable.

What Qualifies as an Error Boundary

React's documentation defines an error boundary as a class component that implements either (or both) of two lifecycle methods:

  • static getDerivedStateFromError()
  • componentDidCatch()

Two constraints follow from this definition. First, only class components can serve as error boundaries; functional components are not supported for this role. Second, the choice of method depends on what you want the boundary to do. static getDerivedStateFromError() is used to update state so that a fallback UI can be rendered, while componentDidCatch() is the place to log error details or send them to a reporting service.

Error boundaries are not a catch-all. They do not intercept errors thrown inside event handlers, asynchronous code such as setTimeout or requestAnimationFrame callbacks, server-side rendering errors, or errors thrown within the boundary itself. A boundary placed higher in the tree could catch an error thrown by a nested boundary, but the boundary outside an error boundary's own logic will not handle its own failures.

Building a Working Boundary

To see the behavior in practice, consider a demo app that fetches images from an API and displays them in one column, with descriptive text and buttons in a second column. Clicking a button labeled Replace string with object swaps a stringified object for a plain JavaScript object in the rendered output. React refuses to render plain objects, and without an error boundary the entire page goes blank, forcing a refresh.

A second button, Invoke event handler, throws an error inside an event handler. React handles this case differently: the error is logged to the console, but the rendered page remains intact and no refresh is needed.

The custom boundary component defines both lifecycle methods:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class ErrorBoundary extends Component {
  state = {
    error: '',
    errorInfo: '',
    hasError: false,
  };
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  componentDidCatch(error, errorInfo) {
    // eslint-disable-next-line no-console
    console.log({ error, errorInfo });
    this.setState({ errorInfo });
  }
  render() {
    // next code block goes here
  return this.props.children;
  }
}
ErrorBoundary.propTypes = {
  children: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]).isRequired,
};

When a child throws, getDerivedStateFromError() receives the error and updates state to flag that a failure has occurred. componentDidCatch() receives the error and an errorInfo object that includes a componentStack key with details about which component threw. That information can be logged or stored as needed.

The render method returns this.props.children when no error is present. After an error, it checks a hasError state flag and renders a fallback UI:

const { hasError, errorInfo } = this.state;
if (hasError) {
  return (
    <div className="card my-5">
      <div className="card-header">
        <p>
          There was an error in loading this page.{' '}
          <span
            style={{ cursor: 'pointer', color: '#0077FF' }}
            onClick={() => {
              window.location.reload();
            }}
          >
            Reload this page
          </span>{' '}
        </p>
      </div>
      <div className="card-body">
        <details className="error-details">
          <summary>Click for error details</summary>
          {errorInfo && errorInfo.componentStack.toString()}
        </details>
      </div>
    </div>
  );
}

The fallback shown in this example includes details about the error and a reload button. For a production environment, displaying raw error text to end users is not advisable; a more user-friendly message would be appropriate.

Wrapping ColumnRight in the boundary changes the failure mode. Clicking Replace string with object now crashes only the right column, leaving the image column functional and the header intact. The fallback UI appears in place of the crashed content, and an error log shows up in the developer console.

Boundaries can be applied at any level of granularity. Wrapping only the specific paragraph that throws, rather than the entire column, preserves even more of the interface. The rest of the column, including the buttons, remains operational, and only the offending paragraph is replaced with the fallback.

Adding Real-Time Monitoring with Sentry

Error boundaries improve the user experience, but they do not eliminate the underlying bugs. Monitoring errors as they happen gives you the information needed to reproduce and fix them. Sentry provides a service for that purpose, and it can be hooked into the lifecycle methods of an error boundary to automatically report failures.

While the error boundary handles rendering-time errors cleanly, its coverage has limits. Errors in event handlers and asynchronous code still escape its net. Sentry's SDK can capture those cases too, but the integration point within an error boundary focuses on errors surfaced through React's rendering lifecycle.

Wiring Sentry Into a React App

Sentry is a commercial error monitoring service with a free developer plan that supports up to 5,000 logged events per month. An event in Sentry's terminology is a crash report — an exception or error. To get started, create an account, then create a new project from the Projects page in the left navigation. Choose React as the platform, enable Alert me on every new issue under default alert settings, name the project, and finish creation.

Next, install the browser SDK:

# install Sentry
yarn add @sentry/browser

Copy the initialization snippet from the Sentry configuration page into index.js:

import * as Sentry from '@Sentry/browser';

# Initialize with Data Source Name (dsn)
Sentry.init({ dsn: 'dsn-string' });

That alone is sufficient for Sentry to report uncaught exceptions. As the Sentry docs note, @Sentry/browser on its own will report any uncaught exceptions triggered from your application.

After you trigger an error in the browser, the issue stream will show the event. The dashboard includes useful metadata such as frequency graphs and assignment tools for delegating issues to team members. Clicking into an individual issue reveals further detail.

To send errors caught by the error boundary to Sentry, update ErrorBoundary.js:

# import Sentry
import * as Sentry from '@sentry/browser'

# add eventId to state
state = {
  error: '',
  eventId: '', // add this to state
  errorInfo: '',
  hasError: false,
};

# update componentDidCatch
componentDidCatch(error, errorInfo) {
  // eslint-disable-next-line no-console
  console.log({ error, errorInfo });
  Sentry.withScope((scope) => {
    scope.setExtras(errorInfo);
    const eventId = Sentry.captureException(error);
    this.setState({ eventId, errorInfo });
  });
}

This uses the Sentry.captureException method, which pushes the error to the Sentry dashboard.

Sentry also provides a user feedback widget. Adding the feedback button to the fallback UI inside the error boundary — for example, right after the div with className card-body — gives users a way to report context around the failure:

<div className="card-body">
  ...
</div>

# add the Sentry button
<button
  className="bg-primary text-light"
  onClick={() =>
    Sentry.showReportDialog({ eventId: this.state.eventId })
  }
>
  Report feedback
</button>

When the fallback UI renders, the Report feedback button displays. Clicking it opens a dialog where users can describe the issue. Submissions appear under User Feedback in the Sentry dashboard.

During development, error alerts can quickly clutter the issue stream. To limit reporting to production events only, enable the Filter out events coming from localhost option under SettingsProjects → your project → Inbound Filters.

Recommendations

Every React app should have an error boundary at the top level, and pairing it with a service like Sentry requires minimal setup. The free plan is enough to get started immediately.

For reference, the code for this integration is available in the branch 03-integrate-sentry. A live demo is hosted on Netlify.

Related documentation: