home / skills / agents-inc / skills / web-error-handling-error-boundaries

web-error-handling-error-boundaries skill

/src/skills/web-error-handling-error-boundaries

This skill helps you implement robust React error boundaries with fallback UIs, recovery resets, and centralized logging for resilient UI.

npx playbooks add skill agents-inc/skills --skill web-error-handling-error-boundaries

Review the files below or copy the command above to add this skill to your agents.

Files (10)
SKILL.md
19.2 KB
---
name: web-error-handling-error-boundaries
description: Error boundary patterns, fallback UI, reset/retry functionality, react-error-boundary library
---

# React Error Boundaries

> **Quick Guide:** Error boundaries catch JavaScript errors in component trees and display fallback UI. Use `react-error-boundary` library for production apps. Place boundaries strategically around features (not just root). Boundaries do NOT catch event handler, async, or SSR errors. **React 19+**: Use `createRoot` options (`onCaughtError`, `onUncaughtError`, `onRecoverableError`) for centralized error logging.

---

<critical_requirements>

## CRITICAL: Before Using This Skill

> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)

**(You MUST use `getDerivedStateFromError` for rendering fallback UI - it runs during render phase)**

**(You MUST use `componentDidCatch` for side effects like logging - it runs during commit phase)**

**(You MUST wrap error boundaries around feature sections, not just the app root)**

**(You MUST provide reset/retry functionality for recoverable errors)**

**(You MUST use `role="alert"` on fallback UI for accessibility)**

**(You MUST use `createRoot` error options (`onCaughtError`, `onUncaughtError`, `onRecoverableError`) for centralized error logging in React 19+)**

</critical_requirements>

---

**Auto-detection:** error boundary, ErrorBoundary, getDerivedStateFromError, componentDidCatch, fallback UI, react-error-boundary, useErrorBoundary, error recovery, error fallback, onCaughtError, onUncaughtError, onRecoverableError, captureOwnerStack

**When to use:**

- Catching and displaying fallback UI for render errors
- Implementing retry/reset functionality after errors
- Preventing entire app crashes from component failures
- Creating isolated failure domains for different features

**Key patterns covered:**

- Class-based error boundary implementation
- `react-error-boundary` library patterns
- `useErrorBoundary` hook for async error handling
- Fallback UI with reset functionality
- Strategic boundary placement (granular vs coarse)
- TypeScript error boundary patterns
- **React 19+**: `createRoot` error options (`onCaughtError`, `onUncaughtError`, `onRecoverableError`)
- **React 19+**: `captureOwnerStack()` for enhanced debugging

**When NOT to use:**

- Event handler errors (use try/catch)
- Async code errors outside components (use try/catch)
- Server-side rendering errors (handle at framework level)
- API request errors (handle in data fetching layer)

**Detailed Resources:**

- For code examples, see [examples/core.md](examples/core.md)
- For React 19 error hooks, see [examples/react-19-hooks.md](examples/react-19-hooks.md)
- For testing patterns, see [examples/testing.md](examples/testing.md)
- For decision frameworks and anti-patterns, see [reference.md](reference.md)

---

<philosophy>

## Philosophy

Error boundaries provide **graceful degradation** - when one component fails, the rest of the application continues working. The key principle is **isolation**: wrap distinct features in separate boundaries so failures are contained. Error boundaries are the ONLY way to catch errors during React rendering; they don't replace try/catch for imperative code but complement it for declarative UI.

**Core principles:**

1. **Isolation over global handling** - Multiple granular boundaries beat one root boundary
2. **Recovery over failure** - Provide reset/retry when possible
3. **User feedback over silent failure** - Show meaningful fallback UI
4. **Logging integration** - Pass errors to monitoring via `onError` callback
5. **Centralized observability (React 19+)** - Use `createRoot` error options for unified error tracking across all boundaries

</philosophy>

---

<patterns>

## Core Patterns

### Pattern 1: Class-Based Error Boundary (Native React)

Error boundaries MUST be class components because `getDerivedStateFromError` and `componentDidCatch` have no hook equivalents.

#### Two Lifecycle Methods

| Method                     | Phase  | Purpose                       | Side Effects |
| -------------------------- | ------ | ----------------------------- | ------------ |
| `getDerivedStateFromError` | Render | Update state to show fallback | NOT allowed  |
| `componentDidCatch`        | Commit | Log errors, call callbacks    | Allowed      |

#### Implementation

```typescript
// ✅ Good Example - Complete error boundary with reset
import { Component } from "react";
import type { ErrorInfo, ReactNode } from "react";

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

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

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

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    // Update state so next render shows fallback UI
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
    // Side effects allowed here - call logging callback
    this.props.onError?.(error, errorInfo);
  }

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

  render(): ReactNode {
    const { hasError, error } = this.state;
    const { children, fallback } = this.props;

    if (hasError && error) {
      // Support both static and function fallback
      if (typeof fallback === "function") {
        return fallback(error, this.handleReset);
      }
      if (fallback) {
        return fallback;
      }
      // Default fallback
      return (
        <div role="alert">
          <h2>Something went wrong</h2>
          <button onClick={this.handleReset}>Try again</button>
        </div>
      );
    }

    return children;
  }
}
```

**Why good:** Separates render-phase logic (getDerivedStateFromError) from commit-phase side effects (componentDidCatch), provides reset capability for recovery, flexible fallback API supports both static and function patterns, onError callback enables logging integration without coupling to specific tools

---

### Pattern 2: react-error-boundary Library

The `react-error-boundary` library provides a production-ready error boundary with hooks support and additional features.

#### Installation

```bash
npm install react-error-boundary
```

#### ErrorBoundary Component Props

| Prop                | Type                    | Purpose                         |
| ------------------- | ----------------------- | ------------------------------- |
| `fallback`          | `ReactNode`             | Static fallback UI              |
| `FallbackComponent` | `ComponentType`         | Component that renders fallback |
| `fallbackRender`    | `(props) => ReactNode`  | Render prop for fallback        |
| `onError`           | `(error, info) => void` | Error logging callback          |
| `onReset`           | `(details) => void`     | Called when boundary resets     |
| `resetKeys`         | `unknown[]`             | Dependencies that trigger reset |

#### Basic Usage

```typescript
// ✅ Good Example - react-error-boundary with FallbackComponent
import { ErrorBoundary } from "react-error-boundary";
import type { FallbackProps } from "react-error-boundary";

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

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, info) => {
        // Send to your error tracking service
        console.error("Boundary caught:", error, info);
      }}
      onReset={() => {
        // Reset app state if needed
      }}
    >
      <Dashboard />
    </ErrorBoundary>
  );
}
```

**Why good:** FallbackComponent pattern enables reusable fallback UI, onError callback decouples error handling from logging implementation, onReset allows cleaning up state before retry

---

### Pattern 3: useErrorBoundary Hook (Async Errors)

Error boundaries don't catch async errors by default. Use `useErrorBoundary` hook to manually trigger the nearest error boundary.

#### The Problem

```typescript
// ❌ This error is NOT caught by error boundary
async function handleClick() {
  const response = await fetch("/api/data");
  if (!response.ok) {
    throw new Error("API failed"); // Lost - boundary doesn't see it
  }
}
```

#### The Solution

```typescript
// ✅ Good Example - useErrorBoundary for async errors
import { useState } from "react";
import { useErrorBoundary } from "react-error-boundary";

function DataLoader() {
  const { showBoundary } = useErrorBoundary();
  const [data, setData] = useState<unknown>(null);

  const handleLoadData = async () => {
    try {
      const response = await fetch("/api/data");
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      const result = await response.json();
      setData(result);
    } catch (error) {
      // Manually trigger nearest error boundary
      showBoundary(error);
    }
  };

  return (
    <div>
      <button onClick={handleLoadData}>Load Data</button>
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}

// Wrap with ErrorBoundary
<ErrorBoundary FallbackComponent={ErrorFallback}>
  <DataLoader />
</ErrorBoundary>
```

**Why good:** showBoundary manually propagates async errors to the nearest boundary, maintains consistent error handling UX across sync and async failures, keeps error handling logic in the error boundary system

#### When to Use showBoundary

- Async operations that should show fallback UI on failure
- Event handlers where errors should propagate to boundary
- Effects that should trigger error UI on failure

#### When NOT to Use showBoundary

- Errors that should be handled locally with inline UI
- Recoverable errors that don't need full fallback UI
- Validation errors that need field-level feedback

---

### Pattern 4: resetKeys for Automatic Reset

Use `resetKeys` to automatically reset the error boundary when certain values change.

```typescript
// ✅ Good Example - Reset boundary when route changes
import { ErrorBoundary } from "react-error-boundary";
import { useLocation } from "react-router-dom";

function AppWithRouteReset() {
  const location = useLocation();

  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      resetKeys={[location.pathname]}
      onResetKeysChange={() => {
        // Optionally track resets
      }}
    >
      <Routes />
    </ErrorBoundary>
  );
}
```

**Why good:** Automatic reset when navigation occurs prevents stale error states, users don't need to manually retry after navigating away and back

#### Common resetKeys Patterns

| Pattern        | Use Case                          |
| -------------- | --------------------------------- |
| `[pathname]`   | Reset on route change             |
| `[selectedId]` | Reset when viewing different item |
| `[retryCount]` | Reset after programmatic retry    |
| `[queryKey]`   | Reset when data source changes    |

---

### Pattern 5: Granular Boundary Placement

Place error boundaries strategically to isolate failures.

#### Placement Strategy

```
App
├─ ErrorBoundary (root - catches unhandled errors)
│   ├─ Header
│   ├─ ErrorBoundary (sidebar - isolated)
│   │   └─ Sidebar
│   ├─ ErrorBoundary (main content - isolated)
│   │   └─ MainContent
│   │       ├─ ErrorBoundary (widget A)
│   │       │   └─ ChartWidget
│   │       └─ ErrorBoundary (widget B)
│   │           └─ TableWidget
│   └─ Footer
```

```typescript
// ✅ Good Example - Granular boundaries around features
function Dashboard() {
  return (
    <div>
      <ErrorBoundary
        fallback={<div>Chart unavailable</div>}
        onError={logError}
      >
        <ChartWidget />
      </ErrorBoundary>

      <ErrorBoundary
        fallback={<div>Table unavailable</div>}
        onError={logError}
      >
        <DataTable />
      </ErrorBoundary>

      <ErrorBoundary
        fallback={<div>Stats unavailable</div>}
        onError={logError}
      >
        <StatsPanel />
      </ErrorBoundary>
    </div>
  );
}
```

**Why good:** One widget failing doesn't crash the entire dashboard, each feature has contextual fallback UI, errors are isolated to their domain

```typescript
// ❌ Bad Example - Single boundary for everything
function Dashboard() {
  return (
    <ErrorBoundary fallback={<div>Dashboard error</div>}>
      <ChartWidget />
      <DataTable />
      <StatsPanel />
    </ErrorBoundary>
  );
}
```

**Why bad:** One failing widget crashes the entire dashboard, users lose access to working features, poor user experience

---

### Pattern 6: Fallback UI Patterns

Design fallback UI that provides context and recovery options.

#### Minimal Fallback

```typescript
// ✅ Good Example - Minimal fallback with retry
function MinimalFallback({ resetErrorBoundary }: FallbackProps) {
  return (
    <div role="alert">
      <p>Failed to load</p>
      <button onClick={resetErrorBoundary}>Retry</button>
    </div>
  );
}
```

#### Detailed Fallback

```typescript
// ✅ Good Example - Detailed fallback with error info
function DetailedFallback({ error, resetErrorBoundary }: FallbackProps) {
  const isDev = process.env.NODE_ENV === "development";

  return (
    <div role="alert">
      <h2>Something went wrong</h2>
      <p>We're working on fixing this issue.</p>

      {isDev && (
        <details>
          <summary>Error details</summary>
          <pre>{error.message}</pre>
          <pre>{error.stack}</pre>
        </details>
      )}

      <div>
        <button onClick={resetErrorBoundary}>Try again</button>
        <button onClick={() => window.location.reload()}>
          Refresh page
        </button>
      </div>
    </div>
  );
}
```

**Why good:** role="alert" announces error to screen readers, error details in development aid debugging, production users see friendly message without technical details, multiple recovery options

#### Feature-Specific Fallback

```typescript
// ✅ Good Example - Contextual fallback
function ChartFallback({ resetErrorBoundary }: FallbackProps) {
  return (
    <div role="alert" className="chart-fallback">
      <span aria-hidden="true">📊</span>
      <p>Chart could not be displayed</p>
      <button onClick={resetErrorBoundary}>Reload chart</button>
    </div>
  );
}

// Usage
<ErrorBoundary FallbackComponent={ChartFallback}>
  <RevenueChart />
</ErrorBoundary>
```

**Why good:** Fallback UI matches the context of the failed component, users understand what feature is unavailable

---

### Pattern 7: React 19+ Error Hooks (createRoot Options)

React 19 introduces three new root-level error handlers that complement error boundaries. These run at the `createRoot` level and provide centralized error logging.

#### Three Error Handlers

| Handler              | When Called                       | Use Case                                  |
| -------------------- | --------------------------------- | ----------------------------------------- |
| `onCaughtError`      | Error caught by an Error Boundary | Log errors that are handled by boundaries |
| `onUncaughtError`    | Error NOT caught by any boundary  | Log/report fatal errors                   |
| `onRecoverableError` | React auto-recovers from error    | Log hydration mismatches, suspense errors |

#### Basic Setup

```typescript
// ✅ Good Example - React 19 createRoot with error handlers
import { createRoot } from "react-dom/client";

const ROOT_ELEMENT_ID = "root";

function logError(
  error: Error,
  errorInfo: { componentStack?: string | null }
) {
  // Send to your error tracking service
  console.error("React error:", error);
  console.error("Component stack:", errorInfo.componentStack);
}

const container = document.getElementById(ROOT_ELEMENT_ID);
if (!container) throw new Error("Root element not found");

const root = createRoot(container, {
  onCaughtError: (error, errorInfo) => {
    // Error caught by an Error Boundary
    logError(error, errorInfo);
  },
  onUncaughtError: (error, errorInfo) => {
    // Error NOT caught - fatal
    logError(error, errorInfo);
  },
  onRecoverableError: (error, errorInfo) => {
    // React auto-recovered (e.g., hydration mismatch)
    logError(error, errorInfo);
  },
});

root.render(<App />);
```

**Why good:** Centralized error logging for ALL React errors, captures errors even when no boundary catches them, provides component stack for debugging

#### With Error Monitoring Service

```typescript
// ✅ Good Example - Integration with error monitoring
import { createRoot } from "react-dom/client";
import * as Sentry from "@sentry/react";

const ROOT_ELEMENT_ID = "root";

const container = document.getElementById(ROOT_ELEMENT_ID);
if (!container) throw new Error("Root element not found");

const root = createRoot(container, {
  onCaughtError: Sentry.reactErrorHandler((error, errorInfo) => {
    // Custom handling for caught errors (optional)
    console.warn("Caught error:", error.message);
  }),
  onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => {
    // Custom handling for uncaught errors (optional)
    console.error("Uncaught error:", error.message);
  }),
  onRecoverableError: Sentry.reactErrorHandler(),
});

root.render(<App />);
```

**Why good:** Error monitoring integration at root level, single point of configuration, `Sentry.reactErrorHandler()` adds proper React context to reports

> **See [examples/react-19-hooks.md](examples/react-19-hooks.md) for complete examples including `captureOwnerStack()` and advanced patterns.**

</patterns>

---

<integration>

## Integration Points

**Error boundaries integrate with your application through callbacks:**

- **`onError`**: Pass errors to your logging/monitoring system
- **`onReset`**: Clean up application state before retry
- **`resetKeys`**: Sync boundary state with application state (routes, selections)

**Error boundaries work alongside:**

- **Try/catch**: For event handlers, async code, imperative operations
- **Data fetching error states**: For API-level errors with retry logic
- **Form validation**: For field-level error display

**Boundaries do NOT replace these patterns - they complement them for rendering errors.**

</integration>

---

<critical_reminders>

## CRITICAL REMINDERS

> **All code must follow project conventions in CLAUDE.md**

**(You MUST use `getDerivedStateFromError` for rendering fallback UI - it runs during render phase)**

**(You MUST use `componentDidCatch` for side effects like logging - it runs during commit phase)**

**(You MUST wrap error boundaries around feature sections, not just the app root)**

**(You MUST provide reset/retry functionality for recoverable errors)**

**(You MUST use `role="alert"` on fallback UI for accessibility)**

**(You MUST use `createRoot` error options (`onCaughtError`, `onUncaughtError`, `onRecoverableError`) for centralized error logging in React 19+)**

**Failure to follow these rules will result in poor error handling, inaccessible UIs, or unrecoverable error states.**

</critical_reminders>

Overview

This skill documents robust React error boundary patterns and practical fallback UI strategies using native class-based boundaries and the react-error-boundary library. It emphasizes recoverable error flows, accessibility, and centralized logging hooks available in React 19+. The goal is to help teams isolate failures, provide retry/reset experience, and integrate with observability tools.

How this skill works

The skill explains class-based ErrorBoundary implementations that use getDerivedStateFromError for render-phase state updates and componentDidCatch for commit-phase side effects like logging. It covers the react-error-boundary package (ErrorBoundary, FallbackComponent, useErrorBoundary) for hook-friendly workflows and useErrorBoundary/showBoundary to surface async or event-handler errors to the nearest boundary. It also documents React 19+ createRoot error options for centralized error reporting.

When to use it

  • Wrap feature sections (widgets, panels, routes) to contain component failures and avoid full-app crashes
  • Present fallback UI with retry/reset behavior for recoverable render or async errors
  • Integrate with logging/monitoring when you need error observability across boundaries
  • Handle async failures (data fetches, event handlers) by forwarding errors to the nearest boundary with showBoundary
  • Reset boundaries automatically on navigation or state change using resetKeys

Best practices

  • Use class-based boundaries: getDerivedStateFromError for fallback rendering and componentDidCatch for logging side effects
  • Place multiple granular boundaries instead of one root boundary to maximize isolation
  • Always provide a reset or retry path and use role="alert" on fallback UI for accessibility
  • Use react-error-boundary for production-ready APIs, FallbackComponent pattern, and useErrorBoundary for async flows
  • In React 19+, configure createRoot options (onCaughtError, onUncaughtError, onRecoverableError) for centralized observability

Example use cases

  • Wrap each dashboard widget in its own ErrorBoundary so one widget failure doesn’t break the rest
  • Use useErrorBoundary/showBoundary inside a data loader to convert fetch failures into fallback UI
  • Provide a MinimalFallback with role="alert" and a retry button that calls resetErrorBoundary
  • Reset an ErrorBoundary automatically when route pathname changes via resetKeys
  • Log errors from componentDidCatch or onError callbacks and correlate with centralized React 19+ root hooks

FAQ

Do error boundaries catch async or event handler errors?

No—by default they only catch render, lifecycle, and constructor errors. Use showBoundary (useErrorBoundary) or try/catch for async and event handler errors.

Where should I place boundaries in my app?

Prefer granular placement around features or widgets to isolate failures. Use a root boundary as a safety net but avoid relying on a single global boundary.