react-errors-boundaries

Installation
SKILL.md

react-errors-boundaries

Quick Reference

Error Boundary Overview

Aspect Detail
Component type Class component ONLY (no hook equivalent exists)
Catches Errors during rendering, lifecycle methods, constructors of child tree
Does NOT catch Event handlers, async code, SSR errors, errors in the boundary itself
React 18 getDerivedStateFromError + componentDidCatch
React 19 Same API + onCaughtError/onUncaughtError callbacks on createRoot
Recommended library react-error-boundary (function component wrapper with hooks)

Lifecycle Methods

Method Phase Purpose Side Effects?
static getDerivedStateFromError(error) Render Return state update to show fallback UI NO — must be pure
componentDidCatch(error, info) Commit Log errors to reporting service YES — side effects allowed

Critical Warnings

NEVER use a function component as an error boundary — React has NO hook equivalent for getDerivedStateFromError. Error boundaries MUST be class components or use the react-error-boundary library.

NEVER rely on error boundaries to catch event handler errors — ALWAYS use try/catch inside event handlers. Error boundaries only catch errors during rendering and lifecycle methods.

NEVER rely on error boundaries to catch async errors (promises, setTimeout) — ALWAYS use .catch() or try/catch in async code. The one exception: errors thrown inside startTransition callbacks ARE caught by error boundaries.

NEVER expect an error boundary to catch its own errors — if the boundary itself throws, a PARENT boundary must catch it. ALWAYS have a top-level boundary as a safety net.

NEVER use error boundaries as a substitute for proper input validation — error boundaries are a safety net for unexpected failures, not a control flow mechanism.

ALWAYS implement both getDerivedStateFromError AND componentDidCatch — the first renders fallback UI, the second logs the error. Using only one gives an incomplete error boundary.

ALWAYS provide a way to recover from errors — a "Try Again" button, navigation link, or key-based reset. A dead-end error screen frustrates users.


Complete TypeScript Error Boundary

import React, { Component, ErrorInfo, ReactNode } from 'react';

interface ErrorBoundaryProps {
  children: ReactNode;
  fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode);
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}

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

  static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
    // Render phase — NO side effects. Return state for fallback UI.
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: ErrorInfo): void {
    // Commit phase — side effects allowed. Log to error service.
    console.error('Error boundary caught:', error);
    console.error('Component stack:', info.componentStack);
    this.props.onError?.(error, info);
  }

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

  render(): ReactNode {
    if (this.state.hasError && this.state.error) {
      if (typeof this.props.fallback === 'function') {
        return this.props.fallback(this.state.error, this.handleReset);
      }
      return this.props.fallback;
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

Usage:

// Static fallback
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
  <UserProfile userId={userId} />
</ErrorBoundary>

// Dynamic fallback with reset
<ErrorBoundary
  fallback={(error, reset) => (
    <div role="alert">
      <p>Error: {error.message}</p>
      <button onClick={reset}>Try Again</button>
    </div>
  )}
  onError={(error, info) => logToService(error, info)}
>
  <Dashboard />
</ErrorBoundary>

What Error Boundaries DO NOT Catch

Scenario Why Not Caught Use Instead
Event handlers Run outside React render cycle try/catch in the handler
Async code (setTimeout, promises) Executes after render completes .catch() or try/catch
Server-side rendering (SSR) Different execution context Server-side error handling
Errors in the boundary itself Cannot catch its own errors A parent error boundary

Exception: Errors thrown inside startTransition callbacks ARE caught by error boundaries.


Boundary Placement Strategy

Decision Tree

Is this the app root?
├── YES → Page-level boundary (catch-all safety net)
│         Shows generic "Something went wrong" with reload option
└── NO → Is this an independent feature/widget?
    ├── YES → Feature-level boundary
    │         Isolates feature failure from rest of page
    └── NO → Is this a single component that might fail?
        ├── YES → Component-level boundary
        │         Granular recovery, minimal UI disruption
        └── NO → No boundary needed here

Placement Levels

// Level 1: Page-level (root safety net) — ALWAYS have this
<ErrorBoundary fallback={<FullPageError />}>
  <App />
</ErrorBoundary>

// Level 2: Feature-level (isolate independent sections)
<ErrorBoundary fallback={<SidebarError />}>
  <Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<FeedError />}>
  <NewsFeed />
</ErrorBoundary>

// Level 3: Component-level (granular recovery)
<ErrorBoundary fallback={<ChartPlaceholder />}>
  <RevenueChart data={data} />
</ErrorBoundary>

ALWAYS have at minimum a page-level error boundary wrapping the entire application.

NEVER wrap every single component in a boundary — this creates excessive overhead and fragments the UI. Use boundaries at natural isolation points.


Error Recovery Patterns

Pattern 1: Key Prop Reset

Force React to unmount and remount the component by changing the key:

function RecoverableWidget({ userId }: { userId: string }) {
  const [errorKey, setErrorKey] = useState<number>(0);

  return (
    <ErrorBoundary
      key={errorKey}
      fallback={
        <button onClick={() => setErrorKey((k) => k + 1)}>
          Retry
        </button>
      }
    >
      <UserWidget userId={userId} />
    </ErrorBoundary>
  );
}

Pattern 2: Reset Callback

Use the boundary's internal reset method (as shown in the complete implementation above):

<ErrorBoundary
  fallback={(error, reset) => (
    <div role="alert">
      <p>Failed to load: {error.message}</p>
      <button onClick={reset}>Try Again</button>
    </div>
  )}
>
  <DataTable />
</ErrorBoundary>

Pattern 3: Navigate Away

Redirect to a safe route on error:

function NavigatingFallback({ error }: { error: Error }) {
  return (
    <div role="alert">
      <p>This page encountered an error.</p>
      <a href="/">Return to Home</a>
    </div>
  );
}

Nested Boundaries

Inner boundaries catch errors first. If the inner boundary itself fails, the outer boundary catches it.

<ErrorBoundary fallback={<AppCrashScreen />}>       {/* Outer: last resort */}
  <Header />
  <ErrorBoundary fallback={<ContentError />}>        {/* Inner: feature-level */}
    <ErrorBoundary fallback={<WidgetError />}>       {/* Innermost: component */}
      <ComplexWidget />
    </ErrorBoundary>
    <OtherContent />
  </ErrorBoundary>
  <Footer />
</ErrorBoundary>

Behavior: If ComplexWidget throws, the innermost boundary shows <WidgetError />. The rest of the page (Header, OtherContent, Footer) remains functional.


react-error-boundary Library

The react-error-boundary package provides a function-component-based API. ALWAYS use this library instead of writing custom class components.

npm install react-error-boundary

ErrorBoundary Component

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

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

<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onError={(error, info) => logToService(error, info)}
  onReset={(details) => {
    // Reset app state that caused the error
  }}
  resetKeys={[userId]}  // Auto-reset when these values change
>
  <UserProfile userId={userId} />
</ErrorBoundary>

useErrorBoundary Hook

Trigger the nearest error boundary from event handlers or async code:

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

function UserActions() {
  const { showBoundary } = useErrorBoundary();

  async function handleDelete() {
    try {
      await deleteUser();
    } catch (error) {
      showBoundary(error);  // Triggers nearest ErrorBoundary
    }
  }

  return <button onClick={handleDelete}>Delete Account</button>;
}

This solves the limitation that error boundaries cannot catch event handler or async errors.


React 19: Root-Level Error Callbacks

React 19 adds error reporting callbacks on createRoot:

import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root')!, {
  onCaughtError: (error, errorInfo) => {
    // Errors caught by an Error Boundary
    console.error('Caught by boundary:', error, errorInfo.componentStack);
  },
  onUncaughtError: (error, errorInfo) => {
    // Errors NOT caught by any Error Boundary
    console.error('Uncaught:', error, errorInfo.componentStack);
  },
  onRecoverableError: (error, errorInfo) => {
    // Errors React recovers from automatically
    console.error('Recovered:', error, errorInfo.componentStack);
  },
});

These callbacks complement error boundaries — they do NOT replace them.


Testing Error Boundaries

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// Component that throws on demand
function ThrowingComponent({ shouldThrow }: { shouldThrow: boolean }) {
  if (shouldThrow) {
    throw new Error('Test error');
  }
  return <p>Content is fine</p>;
}

describe('ErrorBoundary', () => {
  // Suppress console.error noise in test output
  const originalError = console.error;
  beforeAll(() => { console.error = vi.fn(); });
  afterAll(() => { console.error = originalError; });

  it('renders children when no error', () => {
    render(
      <ErrorBoundary fallback={<p>Error occurred</p>}>
        <ThrowingComponent shouldThrow={false} />
      </ErrorBoundary>
    );
    expect(screen.getByText('Content is fine')).toBeInTheDocument();
  });

  it('renders fallback when child throws', () => {
    render(
      <ErrorBoundary fallback={<p>Error occurred</p>}>
        <ThrowingComponent shouldThrow={true} />
      </ErrorBoundary>
    );
    expect(screen.getByText('Error occurred')).toBeInTheDocument();
  });

  it('calls onError callback with error details', () => {
    const onError = vi.fn();
    render(
      <ErrorBoundary fallback={<p>Error</p>} onError={onError}>
        <ThrowingComponent shouldThrow={true} />
      </ErrorBoundary>
    );
    expect(onError).toHaveBeenCalledWith(
      expect.objectContaining({ message: 'Test error' }),
      expect.objectContaining({ componentStack: expect.any(String) })
    );
  });
});

Development vs Production Behavior

Behavior Development Production
Error bubbling Errors bubble to window.onerror even when caught Only caught by error boundaries
Console output React logs full component stack Minimal logging
Strict Mode Components render twice to detect impurities Single render

Reference Links

Official Sources

Related skills
Installs
5
GitHub Stars
1
First Seen
Mar 31, 2026