Chapter 3 of 4

Placing Boundaries

Granularity, recovery, and what the user actually sees.

Where you put boundaries decides how much of the screen a single failure takes with it.

A layered approach

  1. One boundary at the root, so a total failure still shows a branded page rather than a blank screen.
  2. One per route, so navigating away recovers naturally.
  3. One around each independent widget - a chart, a feed, a third-party embed - so one failure does not remove the rest of the page.
<RootErrorBoundary>
  <Layout>
    <RouteErrorBoundary>
      <Dashboard>
        <ErrorBoundary fallback={<WidgetError name="Revenue chart" />}>
          <RevenueChart />
        </ErrorBoundary>
        <ErrorBoundary fallback={<WidgetError name="Activity" />}>
          <ActivityFeed />
        </ErrorBoundary>
      </Dashboard>
    </RouteErrorBoundary>
  </Layout>
</RootErrorBoundary>

Recovering

A boundary that only ever shows a dead end is frustrating. Give the user a way out, and reset the boundary's state so children can try again.

class ErrorBoundary extends React.Component {
  state = { error: null };
  static getDerivedStateFromError(error) { return { error }; }

  reset = () => this.setState({ error: null });

  render() {
    if (this.state.error) {
      return (
        <div role="alert">
          <p>This section could not be loaded.</p>
          <button onClick={this.reset}>Try again</button>
        </div>
      );
    }
    return this.props.children;
  }
}

Logging

componentDidCatch is where errors reach your monitoring tool. Include the component stack - it tells you which component failed, which the JavaScript stack often does not.

componentDidCatch(error, errorInfo) {
  Sentry.captureException(error, {
    contexts: { react: { componentStack: errorInfo.componentStack } },
  });
}

Write a safe error message formatter

Complete errorMessage so it returns error.message when it exists, and a generic fallback otherwise. It must never throw.