Error Boundaries in React

When a component throws an error during rendering, React unmounts the entire component tree and shows a blank page. Error Boundaries catch those errors and let you show a fallback UI instead.

What happens without an Error Boundary

Suppose a component tries to read a property from null:

function UserCard({ user }) {
  return <p>{user.name.toUpperCase()}</p>; // throws if user is null
}

If user is null, this throws a TypeError. Without an Error Boundary, React unmounts the whole app. In development you see React’s error overlay. In production your users see a completely blank page with no explanation.

Writing an Error Boundary

Error Boundaries must be class components. There is no hook that replicates getDerivedStateFromError, so this is one place where a class component is still required.

import { Component } from 'react';

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

  static getDerivedStateFromError() {
    // Called when a child throws. Return new state to show the fallback.
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // Good place to log errors to a service like Sentry.
    console.error('Caught an error:', error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <p>Something went wrong.</p>;
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

getDerivedStateFromError is a static method that receives the thrown error and returns updated state. React calls it during the render phase so you can switch to the fallback immediately.

componentDidCatch runs after the render. Use it to log the error and the component stack trace to an error tracking service.

Using an Error Boundary

Wrap any section of your app that might throw:

import ErrorBoundary from './ErrorBoundary';

function ProfilePage({ userId }) {
  return (
    <ErrorBoundary fallback={<p>Could not load the profile. Try refreshing.</p>}>
      <UserProfile userId={userId} />
    </ErrorBoundary>
  );
}

If UserProfile or any of its children throws, the fallback paragraph is shown instead. The rest of the app outside the boundary continues working normally.

Where to place Error Boundaries

You do not need to wrap the entire app in one boundary. Placing them around individual sections gives users a better experience: one broken section shows an error, and the rest of the page still works.

Useful places to add an Error Boundary:

  • Around each top-level page component (so a broken page does not crash the whole app)
  • Around components that fetch and display data
  • Around third-party widgets you do not control
function App() {
  return (
    <>
      <Navbar />
      <ErrorBoundary fallback={<p>Could not load the sidebar.</p>}>
        <Sidebar />
      </ErrorBoundary>
      <ErrorBoundary fallback={<p>Could not load the main content.</p>}>
        <MainContent />
      </ErrorBoundary>
    </>
  );
}

The react-error-boundary library

The react-error-boundary package provides a ready-made ErrorBoundary component with more features than a hand-written one: an onError callback, a resetKeys array that resets the boundary when specified values change, and a resetErrorBoundary function passed to your fallback component.

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

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div>
      <p>Something went wrong: {error.message}</p>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function ProfilePage({ userId }) {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => refetch()}>
      <UserProfile userId={userId} />
    </ErrorBoundary>
  );
}

For most projects this library is easier to use than writing your own Error Boundary from scratch.

What Error Boundaries do NOT catch

Error Boundaries only catch errors that happen during rendering, in lifecycle methods, and in constructors. They do not catch:

  • Errors in event handlers: use a regular try/catch block inside the handler
  • Errors in async code: use try/catch inside your async function or .catch() on a Promise
  • Errors in server-side rendering: Error Boundaries are a client-side concept
  • Errors in the Error Boundary component itself
// This error is NOT caught by an Error Boundary
function Button() {
  function handleClick() {
    throw new Error('Button exploded'); // event handler error, use try/catch
  }
  return <button onClick={handleClick}>Click</button>;
}

// Use try/catch inside the handler instead:
function Button() {
  function handleClick() {
    try {
      riskyOperation();
    } catch (err) {
      console.error(err);
    }
  }
  return <button onClick={handleClick}>Click</button>;
}

Common mistakes

Expecting Error Boundaries to catch async errors. An unhandled rejection in a useEffect fetch will not be caught by an Error Boundary. Use try/catch in your async functions and store the error in state, then render a fallback based on that state.

One giant Error Boundary around the whole app. This works, but if anything goes wrong, the entire app shows an error. Wrap individual sections so failures are contained.

Not logging errors anywhere. Always use componentDidCatch or the onError prop from react-error-boundary to send error details somewhere useful. A fallback UI that says “something went wrong” without a logged error makes debugging very difficult.