Skip to content

Error Boundaries

intermediate12 min read

Without Error Boundaries, One Bug Crashes Everything

Picture this: you have a beautiful app with 50 components, and one of them throws a TypeError. What happens? A single unhandled error in any component unmounts the entire React tree. The user sees a blank white screen. No error message, no recovery path, nothing.

function BuggyComponent() {
  const data = null;
  return <div>{data.name}</div>; // TypeError: Cannot read property 'name' of null
}

function App() {
  return (
    <div>
      <Header />
      <BuggyComponent /> {/* This error crashes Header and Footer too */}
      <Footer />
    </div>
  );
}
// Result: blank screen. All of App is gone.

Error boundaries prevent this. They catch errors in their child tree and render a fallback UI instead of unmounting everything.

Mental Model

Think of error boundaries as circuit breakers in an electrical system. When a short circuit (error) occurs in one circuit (component subtree), the circuit breaker trips and isolates the failure. The rest of the building (application) keeps running. Without circuit breakers, one failure brings down the entire grid.

Error Boundaries Are Class Components

Yes, you read that right — class components. There is no hooks equivalent for error boundaries. They must be class components that implement either (or both) of two lifecycle methods:

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

  // Called during render phase — update state to show fallback
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  // Called during commit phase — log the error
  componentDidCatch(error, errorInfo) {
    console.error('Error caught by boundary:', error);
    console.error('Component stack:', errorInfo.componentStack);
    // Send to error tracking service (Sentry, etc.)
    reportError(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <DefaultErrorUI error={this.state.error} />;
    }
    return this.props.children;
  }
}

Two Lifecycle Methods, Two Phases

MethodPhasePurpose
getDerivedStateFromErrorRenderReturn new state to trigger fallback UI
componentDidCatchCommitSide effects: logging, error reporting

getDerivedStateFromError is static and pure — no side effects. It runs during the render phase. componentDidCatch runs during the commit phase and is where you send errors to Sentry, LogRocket, or your logging service.

Why no hooks API for error boundaries

Error boundaries need to catch errors thrown during rendering — the render phase. Hooks like useEffect run after rendering. There is no hook equivalent for intercepting errors thrown during the render phase of child components. The React team has discussed useErrorBoundary but has not shipped it. For now, class components are required. Libraries like react-error-boundary provide a convenient wrapper.

What Error Boundaries Catch (and Don't)

Catches

  • Errors thrown during rendering
  • Errors in lifecycle methods
  • Errors in constructors of child components

Does NOT Catch

  • Event handler errors (use try/catch)
  • Asynchronous code (setTimeout, promises)
  • Server-side rendering errors
  • Errors thrown in the boundary itself
function ClickHandler() {
  function handleClick() {
    // Error boundary will NOT catch this:
    throw new Error('Click failed');
  }

  return <button onClick={handleClick}>Click</button>;
}

// For event handlers, use try/catch:
function SafeClickHandler() {
  const [error, setError] = useState(null);

  function handleClick() {
    try {
      riskyOperation();
    } catch (err) {
      setError(err);
    }
  }

  if (error) return <p>Something went wrong: {error.message}</p>;
  return <button onClick={handleClick}>Click</button>;
}
Common Trap

Error boundaries catch errors thrown during React's render phase — not errors in event handlers, async code, or the boundary component itself. If you need to catch errors in event handlers or async operations, use standard try/catch and set error state. The boundary is specifically for catching render-time failures in child components.

Placement Strategy: Granular Boundaries

The thing that separates production apps from tutorials is this: do not wrap the entire app in one boundary. Use multiple boundaries at different granularities:

function App() {
  return (
    <ErrorBoundary fallback={<FullPageError />}>
      {/* App-level: catches catastrophic errors */}
      <Header />
      <main>
        <ErrorBoundary fallback={<SidebarError />}>
          {/* Section-level: sidebar failure does not break content */}
          <Sidebar />
        </ErrorBoundary>
        <ErrorBoundary fallback={<ContentError />}>
          {/* Section-level: content failure does not break sidebar */}
          <Content />
        </ErrorBoundary>
      </main>
      <Footer />
    </ErrorBoundary>
  );
}

Error Recovery

Allow users to recover from errors by resetting the boundary state:

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

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

  componentDidCatch(error, info) {
    reportError(error, info);
  }

  resetError = () => {
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError) {
      return (
        <div role="alert">
          <h2>Something went wrong</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={this.resetError}>Try Again</button>
        </div>
      );
    }
    return this.props.children;
  }
}

Resetting on Navigation

Reset the boundary when the user navigates to a different page:

class ErrorBoundary extends React.Component {
  componentDidUpdate(prevProps) {
    if (prevProps.resetKey !== this.props.resetKey) {
      this.setState({ hasError: false });
    }
  }

  // ... rest of the boundary
}

// Usage with React Router:
function App() {
  const location = useLocation();
  return (
    <ErrorBoundary resetKey={location.pathname}>
      <Routes>{/* ... */}</Routes>
    </ErrorBoundary>
  );
}

Production Scenario: The react-error-boundary Library

Most production apps use the react-error-boundary library for a cleaner API:

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <h2>Something went wrong</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, info) => reportError(error, info)}
      onReset={() => {
        // Reset app state that caused the error
      }}
    >
      <Dashboard />
    </ErrorBoundary>
  );
}
Execution Trace
Error thrown:
Child component throws during render
TypeError, ReferenceError, etc.
Boundary catches:
Nearest ErrorBoundary intercepts the error
React walks up the tree to find a boundary
getDerivedStateFromError:
Returns `( hasError: true )`
Static method, runs during render phase
Fallback render:
Boundary renders fallback UI instead of children
User sees error message, not blank screen
componentDidCatch:
Error logged and reported to service
Runs during commit phase — safe for side effects
Recovery:
User clicks 'Try Again' → state resets → children re-render
Re-attempting the render that failed
Common Mistakes
  • Wrong: Wrapping the entire app in one error boundary Right: Use multiple boundaries at different granularities — page, section, component

  • Wrong: Expecting error boundaries to catch event handler errors Right: Use try/catch in event handlers and set error state

  • Wrong: Not providing error recovery (retry/reset) Right: Include a reset mechanism — button, navigation change, or time-based retry

  • Wrong: Trying to use hooks to create an error boundary Right: Use a class component or the react-error-boundary library

Quiz
Which errors does an ErrorBoundary NOT catch?
Quiz
What is the difference between getDerivedStateFromError and componentDidCatch?
Quiz
What happens without any error boundary when a component throws during render?
Key Rules
  1. 1Error boundaries catch render-phase errors in children — not event handlers or async code
  2. 2Class components only — no hooks API exists for error boundaries
  3. 3getDerivedStateFromError (render phase) for fallback UI, componentDidCatch (commit phase) for logging
  4. 4Place boundaries at multiple granularities — app level, page level, section level
  5. 5Always provide error recovery — retry button, navigation reset, or automatic retry

Challenge: Build a Retry Boundary

Error Boundary with Automatic Retry