Skip to content
C Codeloom
React

React Error Boundaries: A Practical Guide

How to build resilient React apps with Error Boundaries: what they catch, what they miss, and how to design fallback UI that actually helps users.

·5 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • What Error Boundaries catch
  • Why they are still class components
  • How to compose them with Suspense
  • Designing useful fallbacks
  • Logging and reset patterns

Prerequisites

  • Comfortable with React components

An uncaught error during rendering will, by default, unmount your entire React tree. The user sees a blank page, which is rarely what you want. Error Boundaries are React’s mechanism to catch those errors, isolate the broken subtree, and render a fallback instead. They are simple to write, easy to forget, and worth setting up early.

What and why

An Error Boundary is a component that implements getDerivedStateFromError or componentDidCatch. When a descendant throws during render, in a lifecycle method, or in a constructor, the nearest boundary catches it and renders its fallback. The rest of the app keeps running.

What boundaries do not catch: event handlers, asynchronous code (like setTimeout), server-side rendering errors, and errors thrown in the boundary itself. For event handlers and async code, you handle errors with try/catch like normal JavaScript.

Mental model

A boundary is a try/catch for the rendering phase. When a child throws, React walks up the tree until it finds a boundary, then renders that boundary’s fallback in place of the broken subtree.

<ErrorBoundary fallback={<Oops />}>
 |
 v
<Dashboard>
 |
 v
<Chart>          <-- throws during render
 |
 v
React unwinds
 |
 v
ErrorBoundary catches -> renders <Oops />
(rest of app outside boundary is untouched)
Error propagation in React

The granularity matters. A single boundary at the root will replace the whole app on any error. Multiple smaller boundaries let you isolate the damage to a widget.

Hands-on example

A minimal boundary still has to be a class component because React has not shipped a hook equivalent for componentDidCatch.

import { Component, ReactNode } from 'react';

type Props = { fallback: ReactNode; onError?: (e: Error) => void; children: ReactNode };
type State = { hasError: boolean };

export class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false };

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

  componentDidCatch(error: Error, info: { componentStack: string }) {
    this.props.onError?.(error);
    console.error('Caught:', error, info.componentStack);
  }

  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}

Usage with a meaningful fallback and a reset:

function App() {
  const [key, setKey] = useState(0);
  return (
    <ErrorBoundary
      key={key}
      fallback={
        <div>
          <p>Something went wrong loading the chart.</p>
          <button onClick={() => setKey(k => k + 1)}>Try again</button>
        </div>
      }
      onError={(e) => logToService(e)}
    >
      <RevenueChart />
    </ErrorBoundary>
  );
}

Changing the key forces React to unmount and remount the boundary, which resets its internal hasError flag and gives the subtree a fresh start.

Composing with Suspense

Suspense handles “not ready yet”; Error Boundaries handle “broken.” Pair them so each loading region also has a recovery path.

<ErrorBoundary fallback={<ListError />}>
  <Suspense fallback={<ListSkeleton />}>
    <OrderList />
  </Suspense>
</ErrorBoundary>

If the fetch fails, the error boundary shows <ListError />. If it is still loading, Suspense shows the skeleton. The two responsibilities stay separate.

For a smoother developer experience, libraries like react-error-boundary provide a hook-based API and reset helpers.

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

<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onReset={() => refetch()}
  resetKeys={[queryId]}
>
  <Widget />
</ErrorBoundary>

Common pitfalls

  • Wrapping the whole app in one boundary. A single error nukes everything. Use multiple boundaries at meaningful UI seams: routes, panels, widgets.
  • Catching errors in event handlers with a boundary. Boundaries do not catch those; use try/catch inside the handler.
  • Forgetting to log. A boundary that silently swallows errors makes production debugging painful. Always pipe componentDidCatch to your monitoring service.
  • Showing a generic “something went wrong” with no recovery action. Give users a retry button or a path back to a known good state.

Best practices

  • Put boundaries at route level by default, then add more at risky widgets (charts, embeds, third-party content).
  • Log the error and the component stack to your monitoring service. The component stack is more useful than the JavaScript stack for finding the offending component.
  • Use resetKeys or key to allow recovery without a full page reload.
  • For server components in frameworks like Next.js, use the framework-provided error.tsx convention, which maps to the same idea.

FAQ

Do hooks support error boundaries? No native hook exists. You either use a class component or pull in react-error-boundary.

Do boundaries work in SSR? They catch errors during hydration on the client. Server-side render errors are handled differently, typically by the framework’s error page.

Should I rethrow in componentDidCatch? No. Throwing inside a boundary bubbles to the next boundary up and can confuse the error reporting flow.

Can a boundary catch async errors from fetch? Only if the async result causes a render-time throw (for example, when using Suspense and use()). For raw fetch().then() errors, handle them in the callback.