Frontend

React Error Boundaries: Handling Errors Gracefully

Mayur Dabhi
Mayur Dabhi
May 9, 2026
13 min read

Every React application will encounter runtime errors. A forgotten null check, an unexpected API response shape, a third-party library throwing an exception — these things happen in production. Without a safety net, a single component crash can unmount the entire component tree and present users with a blank white screen. React Error Boundaries are that safety net: they catch JavaScript errors anywhere in a child component tree, log them, and render a fallback UI instead of crashing the whole application.

Why This Matters

Before Error Boundaries (introduced in React 16), a JavaScript error inside a component would corrupt React's internal state and produce cryptic errors. Now you can isolate failures to individual subtrees, keeping the rest of your UI functional and giving users a meaningful message instead of a broken screen.

What Are Error Boundaries?

An Error Boundary is a React class component that implements one or both of two special lifecycle methods:

The term "boundary" is apt: an Error Boundary acts like a catch block for its entire subtree. Any unhandled error thrown during rendering, in a lifecycle method, or in the constructor of any child component will bubble up to the nearest Error Boundary ancestor.

<App /> <ErrorBoundary /> Catches errors from children <Sidebar /> Renders fine <UserProfile /> Throws Error! <Footer /> Renders fine Fallback UI rendered error bubbles up

Error Boundaries catch errors from descendant components and render a fallback instead

Creating Your First Error Boundary

Error Boundaries must be class components — there is no hook equivalent for the two lifecycle methods they rely on. However, you write them once and reuse them everywhere, so this is a minor inconvenience.

ErrorBoundary.jsx
import React from 'react';

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

  // Called during render when a descendant throws.
  // Return new state to trigger fallback UI.
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  // Called after render when a descendant threw.
  // Use for side-effects: logging, reporting.
  componentDidCatch(error, errorInfo) {
    console.error('ErrorBoundary caught:', error, errorInfo);
    // logErrorToService(error, errorInfo.componentStack);
  }

  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, error: null })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Using Your Error Boundary

Wrap any component or subtree that might throw with your Error Boundary:

App.jsx
import ErrorBoundary from './ErrorBoundary';
import UserProfile from './UserProfile';
import Dashboard from './Dashboard';

function App() {
  return (
    <div>
      <Navigation />

      {/* Wrap only the part that might fail */}
      <ErrorBoundary>
        <UserProfile userId={userId} />
      </ErrorBoundary>

      {/* Different boundary for a different section */}
      <ErrorBoundary fallback={<p>Dashboard unavailable</p>}>
        <Dashboard />
      </ErrorBoundary>

      <Footer />
    </div>
  );
}
Important: Granularity Matters

Don't wrap your entire app in a single Error Boundary — that's no better than a global try/catch. Place boundaries strategically around individual features so a failure in one section doesn't hide the rest of your UI from the user.

What Error Boundaries Do NOT Catch

Understanding the limits of Error Boundaries is just as important as knowing how to use them. They only catch errors that occur during React's rendering pipeline — not all JavaScript errors.

Error Source Caught by Error Boundary? How to Handle Instead
Errors during rendering Yes Error Boundary handles it
Errors in lifecycle methods Yes Error Boundary handles it
Errors in constructors of children Yes Error Boundary handles it
Errors in event handlers No Use try/catch inside the handler
Async errors (setTimeout, fetch) No Use try/catch with async/await
Server-side rendering errors No Handle in SSR framework (Next.js)
Errors inside the boundary itself No Use a parent Error Boundary
Event handler errors — use try/catch
// Error Boundaries do NOT catch this:
function DeleteButton({ id }) {
  const handleClick = () => {
    // This error won't reach the Error Boundary
    throw new Error('Oops');
  };
  return <button onClick={handleClick}>Delete</button>;
}

// You must use try/catch in event handlers:
function DeleteButton({ id }) {
  const [error, setError] = React.useState(null);

  const handleClick = async () => {
    try {
      await deleteItem(id);
    } catch (err) {
      setError(err.message);
    }
  };

  if (error) return <p className="error">{error}</p>;
  return <button onClick={handleClick}>Delete</button>;
}

Advanced Patterns

Accepting a Custom Fallback via Props

Make your Error Boundary flexible by accepting a fallback prop. This lets each usage site control what the user sees when things go wrong:

Flexible ErrorBoundary.jsx
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

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

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

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

  render() {
    if (this.state.hasError) {
      // Accept a render prop, a fallback element, or use a default
      if (typeof this.props.fallback === 'function') {
        return this.props.fallback({
          error: this.state.error,
          reset: this.reset,
        });
      }
      return this.props.fallback ?? <DefaultErrorFallback />;
    }
    return this.props.children;
  }
}

// Usage with render prop:
<ErrorBoundary
  fallback={({ error, reset }) => (
    <div>
      <p>Widget failed: {error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  )}
  onError={(err, info) => Sentry.captureException(err, { extra: info })}
>
  <ComplexWidget />
</ErrorBoundary>

The react-error-boundary Library

For most projects, the community-maintained react-error-boundary package provides a production-ready, highly flexible Error Boundary that eliminates boilerplate. It also exposes a useErrorBoundary hook that lets functional components manually trigger a nearby boundary.

1

Install the package

Run npm install react-error-boundary in your project.

2

Use ErrorBoundary component

Import and use the pre-built ErrorBoundary with FallbackComponent prop.

3

Use useErrorBoundary hook (optional)

Call showBoundary(error) inside async code to manually trigger the boundary.

react-error-boundary usage
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';

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

// Using useErrorBoundary in an async context
function DataFetcher({ url }) {
  const { showBoundary } = useErrorBoundary();
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(setData)
      .catch(showBoundary); // Delegates to nearest ErrorBoundary
  }, [url, showBoundary]);

  return data ? <Display data={data} /> : <Spinner />;
}

// Wiring it all together
function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => window.location.reload()}
      onError={(error, info) => console.error(error, info)}
    >
      <DataFetcher url="/api/users" />
    </ErrorBoundary>
  );
}

Nesting Error Boundaries Strategically

The most powerful aspect of Error Boundaries is their composability. You can nest them to create granular failure isolation — a dashboard widget crashing shouldn't take down the navigation bar or the sidebar.

App-level ErrorBoundary (last resort) <Navigation /> — no boundary needed (simple, unlikely to throw) ErrorBoundary <Sidebar /> <QuickLinks /> Main Content ErrorBoundary <ChartWidget /> Crashed — isolated! ErrorBoundary <ActivityFeed /> ✓ ErrorBoundary <DataTable /> ✓

Nested Error Boundaries isolate failures — the ChartWidget crash doesn't affect ActivityFeed or DataTable

Testing Error Boundaries

Testing that your Error Boundary renders the fallback when a child throws requires a component that intentionally throws. Jest and React Testing Library make this straightforward.

import { render, screen, fireEvent } from '@testing-library/react';
import ErrorBoundary from './ErrorBoundary';

// A component that throws when "shouldThrow" is true
function Bomb({ shouldThrow }) {
  if (shouldThrow) {
    throw new Error('Boom!');
  }
  return <p>All good</p>;
}

test('renders fallback when child throws', () => {
  render(
    <ErrorBoundary>
      <Bomb shouldThrow={true} />
    </ErrorBoundary>
  );
  expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
});

test('renders children when no error', () => {
  render(
    <ErrorBoundary>
      <Bomb shouldThrow={false} />
    </ErrorBoundary>
  );
  expect(screen.getByText('All good')).toBeInTheDocument();
});
// Reusable "Boom" component for testing
function ThrowError({ message = 'Test error' }) {
  throw new Error(message);
}

// Test reset functionality
test('reset clears the error state', () => {
  const { rerender } = render(
    <ErrorBoundary>
      <ThrowError />
    </ErrorBoundary>
  );

  expect(screen.getByRole('button', { name: /try again/i })).toBeInTheDocument();

  // Simulate the user clicking "Try again"
  fireEvent.click(screen.getByRole('button', { name: /try again/i }));

  // Re-render with a non-throwing child
  rerender(
    <ErrorBoundary>
      <p>Recovered!</p>
    </ErrorBoundary>
  );

  expect(screen.getByText('Recovered!')).toBeInTheDocument();
});
// React logs caught errors to console.error by default.
// Suppress noise in tests with a beforeEach/afterEach spy.
let consoleSpy;

beforeEach(() => {
  consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
  consoleSpy.mockRestore();
});

// Now your test output won't be polluted with expected error logs
test('shows fallback without console noise', () => {
  render(
    <ErrorBoundary>
      <ThrowError message="silent error" />
    </ErrorBoundary>
  );
  expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
  // Optionally verify componentDidCatch was still called:
  expect(consoleSpy).toHaveBeenCalled();
});

Error Reporting in Production

The real power of componentDidCatch is sending error reports to a monitoring service so you know when and why your users are hitting failures. Tools like Sentry, Datadog, or LogRocket integrate directly with Error Boundaries.

ErrorBoundary with Sentry integration
import * as Sentry from '@sentry/react';

class ErrorBoundary extends React.Component {
  state = { hasError: false, eventId: null };

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

  componentDidCatch(error, { componentStack }) {
    const eventId = Sentry.captureException(error, {
      contexts: {
        react: { componentStack },
      },
    });
    this.setState({ eventId });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>An error occurred.</h2>
          <button
            onClick={() =>
              Sentry.showReportDialog({ eventId: this.state.eventId })
            }
          >
            Report this issue
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Sentry also ships its own ErrorBoundary with the same API:
// import { ErrorBoundary } from '@sentry/react';
// It does all of the above automatically.

Error Boundary Checklist for Production Apps

  • Top-level boundary: Always have one root Error Boundary as a last resort fallback.
  • Feature-level boundaries: Wrap independent features (widgets, panels, routes) with their own boundaries.
  • Route-level boundaries: Use boundaries per route so a broken page doesn't crash navigation.
  • Error reporting: Always log to an external service in componentDidCatch.
  • User-friendly fallback: Show a message, not a blank screen. Offer a "Try again" button where sensible.
  • Reset on navigation: Use the resetKeys prop (react-error-boundary) to auto-reset when the user navigates away.
  • Test your boundaries: Write at least one test that verifies the fallback renders on error.
  • Don't swallow errors silently: Even if you show a fallback, always report the original error.

Error Boundaries with React Router

When using React Router v6, you can set an errorElement directly on a route — a built-in form of Error Boundary for route-level errors. However, for component-level errors within a route, you still need your own Error Boundary class.

React Router v6 — errorElement
import { createBrowserRouter, RouterProvider, useRouteError } from 'react-router-dom';

// A route-error component (receives the thrown value via useRouteError)
function RouteErrorPage() {
  const error = useRouteError();
  return (
    <div>
      <h1>Page Error</h1>
      <p>{error.statusText || error.message}</p>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: '/',
    element: <Root />,
    errorElement: <RouteErrorPage />,  // Catches loader/action errors
    children: [
      {
        path: 'dashboard',
        element: (
          // Still use your own ErrorBoundary for component-level errors
          <ErrorBoundary>
            <Dashboard />
          </ErrorBoundary>
        ),
        loader: dashboardLoader,
        errorElement: <RouteErrorPage />,  // Catches loader errors
      },
    ],
  },
]);

export default function App() {
  return <RouterProvider router={router} />;
}

Key Takeaways

Error Boundaries transform your React application from fragile to resilient. Here's what to remember:

Summary

  • Error Boundaries are class components — they rely on getDerivedStateFromError and componentDidCatch, which have no hook equivalents.
  • They only catch rendering-phase errors — event handlers, async code, and SSR errors are out of scope; use try/catch there.
  • Granularity is key — nest multiple boundaries to isolate independent features rather than using one big boundary.
  • Always report errors in production — use Sentry, Datadog, or a similar service inside componentDidCatch.
  • The react-error-boundary package eliminates boilerplate and adds the useErrorBoundary hook for async error delegation.
  • Test your boundaries — a Bomb component that intentionally throws makes this straightforward with React Testing Library.
"The goal of error handling is not to hide problems — it's to contain them so they don't cascade, and surface them so they can be fixed."

Error Boundaries are one of those features that seem optional until the first time production goes dark because of an uncaught render error. Add them early, make them granular, wire them to your error reporting service, and your users will thank you — even if they never know why their experience stayed seamless while something was quietly breaking in the background.

React Errors Boundaries Error Handling JavaScript Frontend
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.