Error Boundaries

Difficulty: Advanced

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 crashing the entire application. Without error boundaries, a single thrown error in any component will unmount the whole React tree, leaving users with a blank white screen.

Error boundaries must be class components - there is no hook equivalent for componentDidCatch and getDerivedStateFromError as of React 18. getDerivedStateFromError is a static method that runs during rendering and returns new state (typically { hasError: true }). componentDidCatch is an instance method that runs after the error has been committed - use it for logging to an error reporting service.

You should place error boundaries strategically. A top-level boundary around your entire app prevents the white screen of death. More granular boundaries around individual routes, widgets, or feature sections let the rest of the app continue working when one section fails. For example, if the chat widget throws, the main content should still be usable.

Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them. They do NOT catch errors in event handlers (use try/catch), asynchronous code (promises, setTimeout), server-side rendering, or errors thrown in the error boundary itself. For event handler errors, you need explicit try/catch blocks.

Recovery is an important UX concern. A good fallback UI includes a message explaining something went wrong and a 'Try Again' button that resets the error boundary state. You can do this by changing the key prop on the boundary (forcing a remount) or by providing a reset method via a render prop or callback.

Code examples

Basic error boundary class component

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

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Error caught by boundary:', error);
    console.error('Component stack:', errorInfo.componentStack);
    // Send to error reporting service
    // logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>Something went wrong</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Try Again
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Usage
<ErrorBoundary>
  <RiskyComponent />
</ErrorBoundary>

getDerivedStateFromError sets the error state during render. componentDidCatch logs the error after commit. The fallback UI includes a reset button.

Granular error boundaries per route

function App() {
  return (
    <ErrorBoundary fallback={<FullPageError />}>
      <Header />
      <main>
        <Routes>
          <Route path="/dashboard" element={
            <ErrorBoundary fallback={<p>Dashboard failed to load</p>}>
              <Dashboard />
            </ErrorBoundary>
          } />
          <Route path="/settings" element={
            <ErrorBoundary fallback={<p>Settings failed to load</p>}>
              <Settings />
            </ErrorBoundary>
          } />
        </Routes>
      </main>
    </ErrorBoundary>
  );
}

The outer boundary catches catastrophic errors. Inner boundaries per route let the header and navigation keep working even if a page crashes.

Error boundary with reset via key

function App() {
  const [errorKey, setErrorKey] = React.useState(0);

  return (
    <ErrorBoundary
      key={errorKey}
      fallback={
        <div>
          <p>Something broke.</p>
          <button onClick={() => setErrorKey(k => k + 1)}>
            Reset & Try Again
          </button>
        </div>
      }
    >
      <Dashboard />
    </ErrorBoundary>
  );
}

Changing the key forces React to unmount and remount the ErrorBoundary, clearing its error state and giving the children a fresh start.

Key points

Concepts covered

error boundary, componentDidCatch, getDerivedStateFromError, fallback UI, error recovery