WebMriya
Back to articles

React Error Boundaries: Make UI Failures Recoverable

A practical guide to React error boundaries: where to place them, what they catch, how to recover, and why one global fallback is not enough.

React
TypeScript
Frontend Architecture
Software Architecture
W
Frontend Architecture & Legacy Modernization
October 6, 2026 · 5 min read

A user opens a billing page, clicks a tab, and gets a blank screen. The API is healthy, the rest of the application still works, and a full page refresh may even fix the problem. Your monitoring shows a React exception, but the product gives the user no useful action except leaving the application.

This is the failure mode error boundaries are meant to contain. They do not prevent every bug, and they do not replace proper debugging. They keep one broken part of a React tree from taking down everything around it, while giving the user a controlled recovery path.

What an error boundary actually catches

A React error boundary catches errors thrown while rendering descendant components, during lifecycle methods, and in constructors. It can render a fallback interface instead of allowing the nearest part of the tree to become unusable.

It does not automatically catch errors in event handlers, asynchronous callbacks, server-side rendering, or errors thrown inside the boundary itself. Those cases need their own handling: try/catch around an action, request-state management, server logging, or framework-specific mechanisms.

Failure locationTypical handling
Component renderError boundary
Button click or form submittry/catch and explicit action state
Data requestLoading, error, retry, and empty states
Server renderingFramework-level error route or boundary
Background taskPromise rejection handling and monitoring

One global boundary is a poor default

A single boundary around the entire application is better than having none, but it creates a large blast radius. A failure in a chart, recommendation panel, or secondary widget can replace the whole page with a generic error screen.

Boundaries should follow meaningful recovery units. Ask what the user can reasonably retry, refresh, or temporarily ignore without losing the rest of the workflow.

  • Place a route-level boundary around each major page or workflow.
  • Add narrower boundaries around complex, optional, or third-party widgets.
  • Keep critical navigation and authentication outside fragile feature areas.
  • Do not wrap every small component; excessive boundaries make failures harder to reason about.

For example, a dashboard can keep its navigation, filters, and primary data table available even if an analytics chart fails. The chart fallback can offer a retry button or explain that the visualization is temporarily unavailable.

A typed boundary needs an explicit recovery strategy

The fallback should not only say that something went wrong. It should answer three practical questions: what failed, whether the user's work is safe, and what action is available now.

A small class component remains a dependable implementation because React error boundaries use the class lifecycle. The important part is not the class itself, but the separation between captured error state, logging, and recovery.

tsx
type BoundaryProps = {
  children: React.ReactNode;
  fallback: (error: Error, retry: () => void) => React.ReactNode;
};

type BoundaryState = {
  error: Error | null;
  resetKey: number;
};

class ErrorBoundary extends React.Component<BoundaryProps, BoundaryState> {
  state: BoundaryState = { error: null, resetKey: 0 };

  static getDerivedStateFromError(error: Error): Partial<BoundaryState> {
    return { error };
  }

  retry = () => {
    this.setState(({ resetKey }) => ({ error: null, resetKey: resetKey + 1 }));
  };

  render() {
    if (this.state.error) {
      return this.props.fallback(this.state.error, this.retry);
    }

    return <React.Fragment key={this.state.resetKey}>{this.props.children}</React.Fragment>;
  }
}

Resetting the boundary remounts its descendants through the changing key. That can clear corrupted local state, but it is not a universal fix. If the same deterministic bug happens during every render, retrying will simply show the fallback again.

Retry is useful only when the failure can change

A retry action makes sense when the problem may be transient: a lazy-loaded chunk failed, a feature flag changed, stale client state became inconsistent, or a dependent widget recovered. It is less useful for a deterministic null-access bug introduced by the latest deployment.

For route and feature boundaries, make the reset respond to a meaningful key such as the route, selected account, or data version. This prevents an old failure from surviving after the user has moved to a different context.

tsx
function BillingPage({ accountId }: { accountId: string }) {
  return (
    <ErrorBoundary
      key={accountId}
      fallback={(error, retry) => (
        <section role="alert">
          <p>Billing could not be displayed.</p>
          <button onClick={retry}>Try again</button>
          <details>
            <summary>Technical details</summary>
            <pre>{error.message}</pre>
          </details>
        </section>
      )}
    >
      <BillingWorkspace accountId={accountId} />
    </ErrorBoundary>
  );
}

Logging must preserve useful context

A boundary is also an observability point. Log the original error together with the route, feature name, release version, user action context, and a safe identifier for the affected resource. Avoid putting tokens, full personal data, or sensitive billing details into client error reports.

Do not silently swallow the exception. A polished fallback without telemetry turns a visible product failure into an invisible maintenance problem. Conversely, do not expose a raw stack trace to users just because it is useful to engineers. Keep diagnostic detail in the monitoring system and show a concise explanation in the interface.

Boundaries are not a substitute for state design

Many apparent component crashes are really missing states. A request that can be loading, rejected, empty, partially available, or stale should be represented explicitly instead of being allowed to throw as a control-flow mechanism.

  • Use request state for expected API failures.
  • Use validation at external data boundaries before rendering assumptions.
  • Use error boundaries for unexpected rendering failures.
  • Test both the fallback and the recovery path, not only the successful render.

This distinction keeps error boundaries focused. They are the last safety net for unexpected UI failures, not a replacement for handling ordinary business errors.

A practical review checklist

  1. Identify the smallest user-visible unit that can fail independently.
  2. Place a boundary around that unit and a broader route-level fallback above it.
  3. Define what retry actually resets and whether remounting is safe.
  4. Provide an accessible fallback with a clear message and action.
  5. Send structured error data to monitoring without leaking sensitive information.
  6. Add a test that throws during render, verifies containment, and exercises recovery.
  7. Check that navigation and critical actions remain available when optional content fails.

A reliable React application does not assume that every render will succeed. It decides where failure should stop, what the user should see, and how the interface can recover without discarding unrelated work. For teams building or stabilizing complex frontends, WebMriya’s Product UI Development capability can help turn those failure boundaries into a deliberate part of the application architecture. For a broader debugging workflow, see Debugging Techniques: What They Are and When to Use Each One and Automation Testing: What It Is For and Where You Actually Need It.

Frequently asked questions

Do error boundaries catch API errors?

Not by themselves. Expected request failures should be represented with loading, error, retry, and empty states; a boundary is for unexpected errors during rendering or lifecycle execution.

Should every React component have its own error boundary?

No. Boundaries should follow meaningful recovery units such as routes, complex widgets, or isolated feature areas. Too many boundaries make the failure model noisy and harder to maintain.

Is retrying an error boundary always safe?

No. Retrying remounts descendants and may clear local state, so it should be used when the failure can plausibly change. Deterministic code bugs will usually fail again.

What should users see in an error-boundary fallback?

Show a concise explanation, preserve surrounding navigation where possible, and provide a useful action such as retrying or returning to a stable view. Keep stack traces and diagnostic details in monitoring tools.