Error Boundaries

Difficulty: Intermediate

Question

What are Error Boundaries in React? How do they work and what are their limitations?

Answer

Error Boundaries are one of those React features that doesn't get enough attention until your app crashes in production and users see a blank white screen. Then suddenly everyone on the team wants to know about them. Let me explain what they are, why they matter, and what their quirks are.

Let's start with the problem. By default, if any component in your React app throws an error during rendering, the ENTIRE component tree unmounts. Your users see a completely blank page. Not a 'something went wrong' message, not a partial page, just white nothingness. That's terrible. In a traditional server-rendered app, an error on one part of the page might break that section but the rest of the page still works. React's default behavior is much more catastrophic.

Error Boundaries fix this. They're React components that act like a catch block for their entire child component tree. When a descendant component throws an error during rendering, the Error Boundary catches it, logs it, and displays a fallback UI instead of letting the whole app crash. The rest of the app outside the boundary continues working normally.

Here's the thing that surprises many developers: Error Boundaries must be class components. Yes, even in 2026, when virtually everything else is written with hooks, Error Boundaries are one of the very last holdouts requiring class syntax. This is because the lifecycle methods getDerivedStateFromError and componentDidCatch have no hooks equivalents. The React team hasn't added them because the use cases are narrow and class components work fine for this purpose.

So how do you build one? An Error Boundary is a class component that implements one or both of these methods:

getDerivedStateFromError(error): This is a static method called during the render phase. Its job is to update state so the next render shows the fallback UI. You return something like { hasError: true, error } from this method. Since it runs during the render phase, it should be pure: no side effects, no logging, no API calls. Just return the new state.

componentDidCatch(error, errorInfo): This is called during the commit phase (after React has updated the DOM). This is where you do side effects: send the error to your logging service (like Sentry, DataDog, or LogRocket), track it in analytics, etc. The errorInfo parameter contains a componentStack property, which is a string showing the component hierarchy that led to the error. This is gold for debugging because it tells you exactly which component threw and which ancestors it was nested under.

In your render method, you check if hasError is true and render a fallback UI, or render this.props.children normally.

The smart pattern is to make your Error Boundary reusable by accepting a fallback prop. Instead of hardcoding the fallback UI, let each usage provide its own fallback component. A chart section can show 'Failed to load chart. Click to retry.' while a sidebar can show 'Sidebar unavailable.' You can also pass the error and a resetError function to the fallback so the user can try again.

Now, here's the critical thing that comes up in every interview: Error Boundaries do NOT catch everything. They have very specific limitations.

What they CATCH: - Errors thrown during rendering (a component's return statement or function body throws) - Errors in lifecycle methods (componentDidMount, etc.) and useEffect callbacks - Errors in constructors of class components

What they DO NOT catch: - Event handlers. If a user clicks a button and the onClick handler throws, the Error Boundary won't catch it. Why? Because event handlers don't happen during rendering. They're asynchronous from React's perspective. Use a regular try-catch inside your event handlers. - Asynchronous code. Errors in setTimeout, setInterval, Promises, or async/await that reject won't be caught. These happen outside React's rendering cycle. - Server-side rendering. Error Boundaries are a client-side concept. - Errors in the Error Boundary itself. If the Error Boundary's render method or getDerivedStateFromError throws, the error propagates to the nearest Error Boundary above it.

Strategic placement of Error Boundaries is important. Think of them as bulkheads in a ship. A ship is divided into watertight compartments so that if one gets flooded, the rest stay dry. Similarly, you should place Error Boundaries around independent sections of your UI:

- At the app root as a last-resort catch-all. This prevents the blank white screen. Show a friendly 'Something went wrong, please refresh' message. - Around each route/page. If the Settings page crashes, the user can still navigate to Dashboard. - Around independent widgets. If the analytics chart fails to render, the rest of the dashboard should still work. - Around third-party components. Libraries you don't control might throw unexpected errors.

Don't wrap every single component in an Error Boundary. That's overkill and clutters your tree. Place them at natural fault lines in your UI.

For a more ergonomic developer experience, many teams use the react-error-boundary library by Brian Vaughn (a former React team member). It provides a ready-made ErrorBoundary component with fallback, fallbackRender, and FallbackComponent props, plus a useErrorBoundary hook that lets function components programmatically trigger the nearest Error Boundary. This is useful for catching async errors: when a Promise rejects in your event handler, you can call showBoundary(error) from the useErrorBoundary hook to trigger the Error Boundary. It bridges the gap between the 'class-only' limitation and the hooks-based world.

In interviews, make sure you can list what Error Boundaries catch and don't catch (this is almost always asked), explain the strategic placement pattern, and mention react-error-boundary as the practical solution for production apps.

Code examples

Basic Error Boundary

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

  static getDerivedStateFromError(error) {
    // Update state to show fallback UI on next render
    return { hasError: true, error };
  }

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

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

// Usage: wrap sections of your app
function App() {
  return (
    <ErrorBoundary>
      <Header />
      <ErrorBoundary>
        <MainContent />
      </ErrorBoundary>
      <ErrorBoundary>
        <Sidebar />
      </ErrorBoundary>
    </ErrorBoundary>
  );
}

Wrap different sections with separate error boundaries so one failing section does not take down the entire page. The retry button resets the error state.

Reusable Error Boundary with Fallback Prop

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

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

  componentDidCatch(error, info) {
    this.props.onError?.(error, info);
  }

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

  render() {
    if (this.state.hasError) {
      // Support custom fallback components
      if (this.props.fallback) {
        const FallbackComponent = this.props.fallback;
        return (
          <FallbackComponent
            error={this.state.error}
            resetError={this.resetError}
          />
        );
      }
      return <p>Something went wrong.</p>;
    }
    return this.props.children;
  }
}

// Custom fallback component
function ChartErrorFallback({ error, resetError }) {
  return (
    <div className="chart-error">
      <p>Failed to load chart: {error.message}</p>
      <button onClick={resetError}>Reload Chart</button>
    </div>
  );
}

// Usage with custom fallback
<ErrorBoundary
  fallback={ChartErrorFallback}
  onError={(err) => trackError(err)}
>
  <AnalyticsChart data={data} />
</ErrorBoundary>

A reusable error boundary accepts a fallback component and an onError callback. Each usage can have a different fallback UI and error handling strategy.

What Error Boundaries Do NOT Catch

function EventHandlerExample() {
  function handleClick() {
    // Error Boundaries do NOT catch this
    // Use try-catch in event handlers
    try {
      riskyOperation();
    } catch (error) {
      console.error('Caught in handler:', error);
      // Show error to user via state
    }
  }

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

function AsyncExample() {
  useEffect(() => {
    // Error Boundaries do NOT catch async errors
    async function fetchData() {
      try {
        const res = await fetch('/api/data');
        if (!res.ok) throw new Error('Failed to fetch');
        return res.json();
      } catch (error) {
        // Handle async error with state
        setError(error);
      }
    }
    fetchData();
  }, []);
}

// What Error Boundaries DO catch:
function BuggyComponent() {
  // 1. Errors during rendering
  const data = null;
  return <p>{data.name}</p>; // TypeError: caught!
}

function BuggyLifecycle() {
  // 2. Errors in lifecycle methods / useEffect
  useEffect(() => {
    throw new Error('Effect error'); // caught!
  }, []);
}

Error boundaries catch rendering errors and lifecycle/effect errors. Event handler and async errors require traditional try-catch handling.

Key points

Concepts covered

Error Boundaries, componentDidCatch, getDerivedStateFromError, Fallback UI