Skip to content
React rc advanced 5 min read

Error Boundaries

An error boundary is a React component that catches JavaScript errors thrown while rendering its child subtree, logs them, and displays a fallback UI instead of letting the whole app crash. Without one, a single uncaught render error unmounts the entire React tree and leaves users staring at a blank screen. Error boundaries let you contain failures to a region of the page, so a broken widget does not take down the navigation, the sidebar, and everything else around it.

Why error boundaries exist

In a component tree, an exception thrown during render normally propagates upward and aborts the commit. Starting with React 16, an unhandled render error unmounts the whole tree on purpose — a half-broken UI is considered worse than no UI, because it can show stale or corrupted data. Error boundaries give you a recovery point: a place to stop the propagation, render something safe, and report the problem.

You typically place boundaries around meaningful sections — a route, a dashboard panel, a comments list — so each region fails independently.

What they catch and what they do not

Error boundaries only catch errors during the render phase, in lifecycle methods, and in constructors of the components below them. They deliberately do not catch several common error sources, because those run outside React’s rendering flow.

Error sourceCaught by an error boundary?How to handle instead
Rendering a child componentYesFallback UI
A child’s lifecycle / hook bodyYesFallback UI
Event handlers (onClick, etc.)Notry/catch in the handler
setTimeout / requestAnimationFrameNotry/catch in the callback
Async code (await, .then)Notry/catch, or .catch()
Errors thrown in the boundary itselfNoA boundary above it
Server-side renderingNo (catch on the server)Wrap renderToString etc.

Warning: The most common surprise is event handlers. A throw inside onClick is a normal JavaScript exception that React does not intercept — wrap risky handler code in try/catch and update state to show the error yourself.

Building an error boundary

There is no hook-based error boundary; this is the one place where a class component is still required, because the behavior depends on static getDerivedStateFromError and componentDidCatch, which have no hook equivalents.

import { Component } from "react";

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

  // Render phase: derive fallback state from the thrown error.
  static getDerivedStateFromError(error) {
    return { error };
  }

  // Commit phase: side effects like logging belong here.
  componentDidCatch(error, info) {
    console.error("Caught by boundary:", error, info.componentStack);
    // sendToErrorService(error, info.componentStack);
  }

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

  render() {
    if (this.state.error) {
      return (
        <div role="alert">
          <p>Something went wrong: {this.state.error.message}</p>
          <button onClick={this.reset}>Try again</button>
        </div>
      );
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

getDerivedStateFromError runs during rendering, so it must be pure — return new state, do nothing else. componentDidCatch runs after the error is committed and is the right place for logging or analytics. Wrap any subtree with it:

function App() {
  return (
    <ErrorBoundary>
      <Dashboard />
    </ErrorBoundary>
  );
}

If Dashboard (or anything it renders) throws during render, the user sees the fallback instead of a white screen.

Output:

Caught by boundary: TypeError: Cannot read properties of undefined (reading 'name')
    at ProfileCard (Dashboard.jsx:12)

Using react-error-boundary

Writing class components by hand is repetitive, so most teams use the react-error-boundary package, which wraps the class logic in a clean, hook-friendly API with built-in reset support.

npm install react-error-boundary
import { ErrorBoundary } from "react-error-boundary";

function Fallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Failed to load: {error.message}</p>
      <button onClick={resetErrorBoundary}>Retry</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={Fallback}
      onError={(error, info) => logError(error, info.componentStack)}
    >
      <Dashboard />
    </ErrorBoundary>
  );
}

It also exposes useErrorBoundary, which lets you forward errors from outside render — such as an async fetch — into the nearest boundary:

import { useErrorBoundary } from "react-error-boundary";
import { useEffect } from "react";

function Profile({ id }) {
  const { showBoundary } = useErrorBoundary();

  useEffect(() => {
    fetch(`/api/users/${id}`)
      .then((res) => res.json())
      .catch(showBoundary); // routes the async error to the boundary
  }, [id, showBoundary]);

  return <p>Loading…</p>;
}

Resetting after an error

A boundary stays in its error state until you reset it. The retry button above clears local state, but if the same bad props produce the same error, you will just fail again. Use resetKeys to automatically recover when a relevant value changes — for example, retrying when the user navigates to a different record.

<ErrorBoundary
  FallbackComponent={Fallback}
  resetKeys={[userId]} // reset whenever userId changes
  onReset={() => queryClient.invalidateQueries(["user", userId])}
>
  <Profile id={userId} />
</ErrorBoundary>

Tip: Pair error boundaries with <Suspense>. Suspense handles the loading state of a subtree while an error boundary handles its failed state — together they cover both async outcomes cleanly.

Best practices

  • Place boundaries around independent regions (routes, panels, widgets) rather than wrapping the whole app in one, so failures stay contained.
  • Use componentDidCatch (or onError) to report errors to a logging service — boundaries are your last chance to capture stack and component-stack data.
  • Remember boundaries do not catch event-handler, async, or SSR errors; handle those with try/catch and surface them via state.
  • Prefer react-error-boundary over hand-rolled classes for reset keys, useErrorBoundary, and a tested API.
  • Provide a real recovery path (retry button or resetKeys) instead of a dead-end message.
  • Keep fallback UIs lightweight and side-effect free so they cannot throw and trigger a parent boundary.
  • In development, expect React’s overlay to also show the error; in production only your fallback appears.
Last updated June 14, 2026
Was this helpful?