React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

error-boundaryerror-handlingrecoveryfallbackreact-patterns

Error Boundaries — Catch rendering errors in the component tree and display fallback UI

Recipe

import { Component, type ErrorInfo, type ReactNode } from "react";
 
class ErrorBoundary extends Component<
  { children: ReactNode; fallback: ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };
 
  static getDerivedStateFromError(): { hasError: boolean } {
    return { hasError: true };
  }
 
  componentDidCatch(error: Error, info: ErrorInfo) {
    console.error("ErrorBoundary caught:", error, info.componentStack);
  }
 
  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}
 
// Usage
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
  <RiskyComponent />
</ErrorBoundary>

When to reach for this: Wrap any component subtree that might throw during rendering. Essential around data-fetching components, third-party widgets, user-generated content renderers, and route-level layouts.

Working Example

import {
  Component,
  useState,
  useCallback,
  type ErrorInfo,
  type ReactNode,
} from "react";
 
// --- Full-featured error boundary with recovery ---
 
interface ErrorBoundaryProps {
  children: ReactNode;
  fallback?: ReactNode;
  onError?: (error: Error, info: ErrorInfo) => void;
  renderFallback?: (props: {
    error: Error;
    reset: () => void;
  }) => ReactNode;
  resetKeys?: unknown[];
}
 
interface ErrorBoundaryState {
  error: Error | null;
}
 
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  state: ErrorBoundaryState = { error: null };
 
  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { error };
  }
 
  componentDidCatch(error: Error, info: ErrorInfo) {
    this.props.onError?.(error, info);
  }
 
  componentDidUpdate(prevProps: ErrorBoundaryProps) {
    if (this.state.error && prevProps.resetKeys !== this.props.resetKeys) {
      // If resetKeys changed, attempt recovery
      const changed = this.props.resetKeys?.some(
        (key, i) => key !== prevProps.resetKeys?.[i]
      );
      if (changed) this.reset();
    }
  }
 
  reset = () => {
    this.setState({ error: null });
  };
 
  render() {
    const { error } = this.state;
    const { children, fallback, renderFallback } = this.props;
 
    if (error) {
      if (renderFallback) {
        return renderFallback({ error, reset: this.reset });
      }
      return fallback ?? <p>Something went wrong.</p>;
    }
 
    return children;
  }
}
 
// --- Usage with recovery UI ---
 
function UserProfile({ userId }: { userId: string }) {
  return (
    <ErrorBoundary
      resetKeys={[userId]}
      onError={(error) => {
        // Report to error tracking service
        reportToSentry(error);
      }}
      renderFallback={({ error, reset }) => (
        <div className="p-6 bg-red-50 rounded-lg text-center">
          <h3 className="text-red-800 font-semibold">Failed to load profile</h3>
          <p className="text-red-600 mt-2">{error.message}</p>
          <button
            onClick={reset}
            className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
          >
            Try Again
          </button>
        </div>
      )}
    >
      <ProfileContent userId={userId} />
    </ErrorBoundary>
  );
}
 
// --- Granular error boundaries per section ---
 
function DashboardPage() {
  return (
    <div className="grid grid-cols-2 gap-4">
      <ErrorBoundary fallback={<WidgetError name="Revenue" />}>
        <RevenueChart />
      </ErrorBoundary>
      <ErrorBoundary fallback={<WidgetError name="Users" />}>
        <UserStats />
      </ErrorBoundary>
      <ErrorBoundary fallback={<WidgetError name="Activity" />}>
        <ActivityFeed />
      </ErrorBoundary>
    </div>
  );
}
 
function WidgetError({ name }: { name: string }) {
  return (
    <div className="p-4 border border-red-200 rounded bg-red-50 text-red-700">
      {name} widget failed to load.
    </div>
  );
}

What this demonstrates:

  • Render-prop fallback with access to the error object and a reset function
  • resetKeys prop that auto-resets the boundary when dependencies change (e.g., route params)
  • Error reporting hook (onError) for Sentry or similar services
  • Granular boundaries so one broken widget doesn't take down the entire page

Deep Dive

How It Works

  • Error boundaries are class components that implement static getDerivedStateFromError() or componentDidCatch().
  • getDerivedStateFromError runs during render to set error state and trigger fallback UI.
  • componentDidCatch runs after commit for side effects like error logging.
  • Error boundaries catch errors thrown during rendering, in lifecycle methods, and in constructors of their child tree.
  • They do NOT catch errors in event handlers, async code, server-side rendering, or errors thrown in the boundary itself.

Parameters & Return Values

Lifecycle MethodWhen CalledPurpose
getDerivedStateFromError(error)During renderReturn new state to trigger fallback UI
componentDidCatch(error, info)After commitSide effects: logging, error reporting
info.componentStackIn componentDidCatchString showing the component tree path

Variations

Hook-based error boundary wrapper — use a package or thin wrapper for hook ergonomics:

// With react-error-boundary (popular library)
import { ErrorBoundary } from "react-error-boundary";
 
function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => queryClient.invalidateQueries()}
      resetKeys={[routeKey]}
    >
      <AppContent />
    </ErrorBoundary>
  );
}
 
function ErrorFallback({
  error,
  resetErrorBoundary,
}: {
  error: Error;
  resetErrorBoundary: () => void;
}) {
  return (
    <div role="alert">
      <p>Error: {error.message}</p>
      <button onClick={resetErrorBoundary}>Retry</button>
    </div>
  );
}

Nested boundaries with different granularity:

// Page-level boundary catches everything
<ErrorBoundary fallback={<FullPageError />}>
  <Layout>
    {/* Section-level boundaries isolate failures */}
    <ErrorBoundary fallback={<SectionError />}>
      <Sidebar />
    </ErrorBoundary>
    <ErrorBoundary fallback={<SectionError />}>
      <MainContent />
    </ErrorBoundary>
  </Layout>
</ErrorBoundary>

TypeScript Notes

  • Type the state as { error: Error | null } rather than { hasError: boolean } to preserve the error object for the fallback.
  • Use ErrorInfo from React for the componentDidCatch second parameter.
  • The componentStack property on ErrorInfo is a string, not a structured object.

Gotchas

  • Event handler errors are not caught — Error boundaries only catch errors during React rendering. An onClick that throws will not be caught. Fix: Use try/catch in event handlers and set local error state.

  • Async errors are not caught — Promises that reject inside useEffect or event handlers are not caught. Fix: Catch async errors and either set local state or re-throw during render (e.g., store in state and throw in render).

  • No hook-based error boundaries — React does not provide a hook equivalent of getDerivedStateFromError. Fix: Use the class component pattern or the react-error-boundary library.

  • Error boundary itself throwing — If the error boundary's own render method throws, it propagates to the nearest parent boundary. Fix: Keep error boundary render methods simple. Never throw in the fallback UI.

  • Recovery without unmount — Resetting hasError to false without remounting children may leave them in a broken state. Fix: Use a key prop on the children wrapper to force remount on reset.

Alternatives

ApproachTrade-off
Error boundariesCatches render errors; requires class component
react-error-boundaryHook-friendly API, resetKeys; extra dependency
Try/catch in event handlersManual; doesn't catch render errors
Suspense with error handlingHandles async loading errors via thrown promises
Global window.onerrorCatches uncaught errors; no React-specific recovery

Real-World Example

From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).

// Production example: Next.js error.tsx with logging, retry, and full-page reload fallback
// File: app/error.tsx
'use client';
 
import { useEffect } from 'react';
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log to external error tracking service
    console.error('Application error:', error);
    // reportToSentry(error);
  }, [error]);
 
  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="text-center space-y-4">
        <h2 className="text-2xl font-bold">Something went wrong</h2>
        {error.digest && (
          <p className="text-sm text-gray-500">Error ID: {error.digest}</p>
        )}
        <div className="flex gap-3 justify-center">
          <button
            onClick={reset}
            className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
          >
            Try again
          </button>
          <button
            onClick={() => window.location.href = '/'}
            className="px-4 py-2 border rounded hover:bg-gray-50"
          >
            Go home
          </button>
        </div>
      </div>
    </div>
  );
}

What this demonstrates in production:

  • error.tsx is a Next.js App Router convention that automatically wraps the page in an error boundary. You do not need to write a class-based error boundary yourself.
  • The useEffect logs the error on mount. This is the correct place to send errors to Sentry or a similar service. Do not log in the render body because it runs during React's render phase.
  • reset() re-renders the route segment, which retries the failed component. This works well for transient errors like network failures or race conditions.
  • error.digest is a hashed error identifier that Next.js generates for server-side errors. It is safe to display to users (it does not expose stack traces or sensitive details). In development, the full error is shown instead.
  • The "Go home" button uses window.location.href instead of router.push() as a deliberate choice. If the error corrupted the React tree or client-side router state, router.push() might fail or render the same broken page. A full page reload via window.location.href guarantees a clean start.
  • This error.tsx must be a Client Component ('use client'). Error boundaries require client-side React to catch and recover from errors.

FAQs

What is an error boundary and what errors does it catch?
  • An error boundary is a class component that catches errors during rendering, lifecycle methods, and constructors of its child tree.
  • It displays a fallback UI instead of crashing the entire app.
  • It does NOT catch errors in event handlers, async code, SSR, or errors in the boundary itself.
Why are error boundaries class components and not hooks?
  • React does not provide a hook equivalent of getDerivedStateFromError or componentDidCatch.
  • These lifecycle methods are only available in class components.
  • Use the react-error-boundary library if you prefer a hook-friendly wrapper API.
What is the difference between getDerivedStateFromError and componentDidCatch?
  • getDerivedStateFromError runs during the render phase to update state and trigger the fallback UI. It must be a pure function.
  • componentDidCatch runs after the commit phase for side effects like logging errors to Sentry.
  • Use both: one for showing the fallback, one for reporting.
How do resetKeys work for automatic error recovery?
<ErrorBoundary
  resetKeys={[userId]}
  renderFallback={({ error, reset }) => (
    <div>
      <p>{error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  )}
>
  <ProfileContent userId={userId} />
</ErrorBoundary>
  • When resetKeys change (e.g., userId changes), the boundary automatically resets and retries rendering.
  • This pairs well with route parameter changes to recover from stale errors.
Gotcha: Why don't error boundaries catch errors in event handlers?
  • Error boundaries only catch errors thrown during React's rendering lifecycle.
  • An onClick handler that throws does not interrupt rendering; it throws in the browser's event loop.
  • Fix: use try/catch inside event handlers and set local error state.
Gotcha: What happens if the error boundary's own render method throws?
  • The error propagates to the nearest parent error boundary.
  • If no parent boundary exists, the entire app crashes.
  • Fix: keep error boundary render methods and fallback UI simple. Never throw in the fallback.
How do you type an error boundary component in TypeScript?
interface ErrorBoundaryState {
  error: Error | null;
}
 
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  state: ErrorBoundaryState = { error: null };
 
  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { error };
  }
}
  • Type state as { error: Error | null } to preserve the error object for the fallback.
  • Use ErrorInfo from React for componentDidCatch's second parameter.
What is error.digest in Next.js error boundaries?
  • error.digest is a hashed error identifier generated by Next.js for server-side errors.
  • It is safe to display to users because it does not expose stack traces or sensitive details.
  • In development, the full error is shown instead.
Why does the Next.js error.tsx "Go home" button use window.location.href instead of router.push()?
  • If the error corrupted the React tree or client-side router state, router.push() might fail or render the same broken page.
  • window.location.href triggers a full page reload, guaranteeing a clean start.
  • This is a deliberate production safety pattern.
How should you structure error boundaries for granular error isolation?
  • Place a page-level boundary to catch any uncaught errors as a last resort.
  • Place section-level boundaries around individual widgets or features so one failure does not take down the entire page.
  • Each boundary can show its own localized fallback UI.
How do you reset an error boundary without leaving children in a broken state?
  • Simply resetting hasError to false re-renders the same children, which may still be broken.
  • Fix: use a key prop on the children wrapper to force a full unmount and remount on reset.
  • This ensures children start fresh with clean state.
  • Suspense — Loading state boundaries that pair with error boundaries
  • State Machines — Model error states explicitly in state transitions
  • Composition — Error boundaries use the composition pattern with children