Error boundaries

React · Advanced · 12 min

Without error boundaries, one throwing component unmounts the entire app and leaves users with a blank page.

If a component throws during rendering, React can't produce a consistent UI, so by default it unmounts the whole tree. An error boundary limits the damage: it catches errors thrown while rendering its descendants (including in their lifecycle and effects, and rejected promises read with use) and renders a fallback instead.

Error boundaries are still written as class components, the one place modern code needs them. A class becomes a boundary by defining static getDerivedStateFromError(error) (returns state to render the fallback) and optionally componentDidCatch(error, info) (for logging; info.componentStack shows where). Most teams use the small `react-error-boundary` package instead of writing one:

<ErrorBoundary fallback={<p>Something went wrong.</p>}>
  <Comments />
</ErrorBoundary>

Error boundaries do not catch errors in event handlers (those don't happen during rendering; use try/catch and set state), in async code outside rendering (setTimeout, a fetch in a handler), or in the boundary itself.

Placement is a design choice, like Suspense boundaries: one at the top as a last resort, plus finer ones around independent widgets (a sidebar, a chart, a route) so a failure in one doesn't take down the rest. Offer a retry: react-error-boundary gives the fallback a resetErrorBoundary function, and changing the boundary's key also remounts it fresh.

React 19 improves reporting: errors are logged once rather than re-thrown and duplicated, and createRoot accepts onCaughtError (errors caught by a boundary), onUncaughtError and onRecoverableError options, which is the right hook for sending errors to a monitoring service.

Example

import { Component, Suspense, use } from "react";

class ErrorBoundary extends Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    return { error };                     // render fallback next
  }
  componentDidCatch(error, info) {
    console.error("Caught:", error, info.componentStack);   // report to monitoring
  }
  render() {
    if (this.state.error) {
      return (
        <div role="alert">
          <p>Couldn't load this section.</p>
          <button onClick={() => this.setState({ error: null })}>Try again</button>
        </div>
      );
    }
    return this.props.children;
  }
}

const failing = Promise.reject(new Error("503 from API"));
function Comments() {
  const comments = use(failing);          // rejection is thrown to the boundary
  return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}

export default function Post() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<p>Loading comments...</p>}>
        <Comments />
      </Suspense>
    </ErrorBoundary>
  );
}

Remember

  • Error boundaries catch render-time errors in their subtree and show a fallback
  • They're class components (getDerivedStateFromError); react-error-boundary wraps this nicely
  • They don't catch event-handler or unrelated async errors; use try/catch there

Watch out for

  • Expecting an error boundary to catch an exception thrown inside an onClick handler.

Check yourself

  1. A button's onClick throws an error. The button is inside an error boundary. What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary