Advanced25 min read

Error Boundaries

Learn how to gracefully handle runtime errors in React using error boundaries — class components that catch JavaScript errors in their child component tree and display a fallback UI.

What Are Error Boundaries?

In a normal JavaScript application, a single unhandled error can crash the entire page. React applications face the same problem — if a component throws an error during rendering, the entire component tree unmounts, leaving the user with a blank screen.

Error boundaries solve this by catching JavaScript errors anywhere in their child component tree, logging those errors, and displaying a fallback UI instead of crashing the whole application.

Think of error boundaries like a try...catch block, but for React components. Just as you wrap risky code in try/catch to prevent your program from crashing, you wrap risky components in an error boundary to prevent your UI from going blank.

Key points about error boundaries:

  • They 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 regular try/catch for those), asynchronous code (setTimeout, requestAnimationFrame), server-side rendering, or errors thrown in the error boundary itself.
  • They work like a JavaScript catch block — the closest error boundary above a failing component will handle the error.
  • You can nest error boundaries to protect different sections of your UI independently.

Why Class Components? The Error Boundary API

Error boundaries are one of the few remaining use cases for class components in modern React. As of React 19, there is still no hook equivalent for catching render errors. The React team has discussed adding one, but it has not shipped yet.

A class component becomes an error boundary when it defines one or both of these lifecycle methods:

static getDerivedStateFromError(error)

  • Called during the render phase after a descendant throws an error.
  • Receives the error that was thrown.
  • Returns an object to update state, which triggers a re-render with the fallback UI.
  • Must be a pure function — no side effects.
jsx
static getDerivedStateFromError(error) {
  // Update state so the next render shows the fallback UI
  return { hasError: true };
}

componentDidCatch(error, errorInfo)

  • Called during the commit phase after a descendant throws an error.
  • Receives two arguments: the error and an object with a componentStack property showing which component in the tree threw.
  • Use this for side effects like logging the error to an external service.
jsx
componentDidCatch(error, errorInfo) {
  // Log the error to an error reporting service
  console.error('Error caught:', error);
  console.error('Component stack:', errorInfo.componentStack);
}

You typically use getDerivedStateFromError to render the fallback UI and componentDidCatch to log the error. Both methods can be used together in the same component.

Building a Reusable Error Boundary

A well-designed error boundary is reusable across your application. Here is a complete pattern:

jsx
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('ErrorBoundary caught:', error, errorInfo);
  }

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

Usage:

jsx
<ErrorBoundary fallback={<p>Oops! This section failed to load.</p>}>
  <RiskyComponent />
</ErrorBoundary>

Notice the fallback prop — it lets you customize the error message for each usage without modifying the error boundary itself. The this.props.children pattern renders the child components normally when there is no error.

Some libraries, like react-error-boundary, provide a ready-made component with more features like error recovery and reset. But understanding how to build one from scratch is essential because:

  1. You understand what happens under the hood.
  2. You can customize the behavior precisely.
  3. It is a common interview question.

Error Recovery and Reset Strategies

Showing a fallback UI is only half the solution — you also want to give users a way to recover from the error. Common strategies include:

1. Retry button:

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

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

  handleReset = () => {
    this.setState({ hasError: false });
  };

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <p>Something went wrong.</p>
          <button onClick={this.handleReset}>Try Again</button>
        </div>
      );
    }
    return this.props.children;
  }
}

When the user clicks "Try Again", the state resets and React attempts to re-render the children. If the error was transient (like a network issue), the re-render may succeed.

2. Key-based reset: Changing the key prop on an error boundary forces React to unmount and remount it, resetting its state:

jsx
<ErrorBoundary key={resetKey}>
  <DataComponent />
</ErrorBoundary>

3. Navigate away: In a routed application, you can redirect the user to a safe page when an error occurs.

Important: If the underlying bug is not fixed, the component will throw the same error again on re-render. Recovery strategies work best for transient errors (network failures, race conditions) rather than logic bugs.

Strategic Placement of Error Boundaries

Where you place error boundaries matters. You should not wrap your entire app in a single error boundary — that is like catching every exception at the top level. Instead, use a layered approach:

1. Top-level boundary — Catch truly unexpected errors and show a full-page error screen:

jsx
<ErrorBoundary fallback={<FullPageError />}>
  <App />
</ErrorBoundary>

2. Route-level boundaries — Isolate errors to individual pages so navigation still works:

jsx
<Route path="/dashboard">
  <ErrorBoundary fallback={<p>Dashboard failed to load.</p>}>
    <Dashboard />
  </ErrorBoundary>
</Route>

3. Feature-level boundaries — Protect specific widgets so the rest of the page remains functional:

jsx
function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <ErrorBoundary fallback={<p>Chart unavailable.</p>}>
        <AnalyticsChart />
      </ErrorBoundary>
      <ErrorBoundary fallback={<p>Feed unavailable.</p>}>
        <ActivityFeed />
      </ErrorBoundary>
    </div>
  );
}

This way, if the analytics chart crashes, the activity feed and the rest of the dashboard remain visible.

Limitations to remember:

  • Error boundaries do not catch errors in event handlers — use try/catch inside the handler.
  • They do not catch errors in asynchronous code — use .catch() or try/catch in async functions.
  • They do not catch errors in server-side rendering.
  • They do not catch errors thrown in the error boundary itself — it needs a parent boundary for that.

Complete Error Boundary Example

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  class ErrorBoundary extends React.Component {
    constructor(props) {
      super(props);
      this.state = { hasError: false };
    }

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

    componentDidCatch(error, errorInfo) {
      console.error("Caught by ErrorBoundary:", error);
    }

    render() {
      if (this.state.hasError) {
        return <div id="error">Something went wrong</div>;
      }
      return this.props.children;
    }
  }

  function BuggyComponent() {
    throw new Error("I crashed!");
    return <p>This will never render</p>;
  }

  function SafeComponent() {
    return <p>I am working fine!</p>;
  }

  function App() {
    return (
      <div>
        <h1>Error Boundary Demo</h1>
        <ErrorBoundary>
          <BuggyComponent />
        </ErrorBoundary>
        <SafeComponent />
      </div>
    );
  }

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

Why must error boundaries be class components in React?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.