git:20260316.00cb75b to git:20260906.3dc53ce

127 added, 335 removed. Audit A to A.

---
name: web-error-handling-error-boundaries
description: Error boundary patterns, fallback UI, reset/retry, react-error-boundary library, React 19 createRoot error hooks
---
# React Error Boundaries
- > **Quick Guide:** Error boundaries catch JavaScript errors in component trees and display fallback UI. Use `react-error-boundary` library (v6+) for production apps. Place boundaries strategically around features, not just root. Boundaries do NOT catch event handler, async, or SSR errors -- use `showBoundary()` hook for async. **React 19+**: Use `createRoot` options (`onCaughtError`, `onUncaughtError`, `onRecoverableError`) for centralized error logging.
+ > **Quick Guide:** A boundary catches errors thrown during render, in lifecycle methods and in constructors, and swaps the subtree for fallback UI. It never sees event-handler, async or server-render errors — those reach it only when something calls `showBoundary()`. Boundaries are class components, because `getDerivedStateFromError` and `componentDidCatch` have no hook equivalent. React 19 adds `onCaughtError`, `onUncaughtError` and `onRecoverableError` on `createRoot`, which log rather than render and are silently ignored on React 18.
- ---
+ **Detailed Resources:**
- <critical_requirements>
+ - [examples/core.md](examples/core.md) — the class boundary, `react-error-boundary` usage, `resetKeys`, `useErrorBoundary`, granular placement
+ - [examples/react-19-hooks.md](examples/react-19-hooks.md) — `createRoot`/`hydrateRoot` error options, `captureOwnerStack()`, error filtering
+ - [examples/recovery.md](examples/recovery.md) — retry limits, exponential backoff, error classification
+ - [examples/testing.md](examples/testing.md) — what makes a boundary testable, and the fixtures that do it
+ - [reference.md](reference.md) — what boundaries catch, lifecycle and prop tables, checklists
- ## CRITICAL: Before Using This Skill
+ ---
- > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
+ ## Which path applies
- **(You MUST use `getDerivedStateFromError` for rendering fallback UI - it runs during render phase)**
+ - **No boundary dependency wanted** — write the class yourself; `getDerivedStateFromError` plus
+ `componentDidCatch` is the whole API, and [examples/core.md](examples/core.md) has it in full.
+ - **`react-error-boundary` is available** — take `resetKeys`, `useErrorBoundary` and the
+ `FallbackProps` type rather than reimplementing them, and follow
+ [examples/core.md](examples/core.md).
+ - **React 19, and the question is logging rather than UI** — the three `createRoot` options report
+ every error including the ones no boundary caught; see
+ [examples/react-19-hooks.md](examples/react-19-hooks.md).
- **(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)**
+ <critical_requirements>
- **(You MUST provide reset/retry functionality for recoverable errors)**
+ ## Before writing error boundary code
- **(You MUST use `role="alert"` on fallback UI for accessibility)**
+ **Return new state from `getDerivedStateFromError` and put every side effect in `componentDidCatch`.** The first runs during render, where a fetch or a log call breaks React's phase rules; the second runs at commit, where they are safe.
- </critical_requirements>
+ **Wrap each feature area in its own boundary as well as the root.** A single root boundary turns one failing widget into a blank page, and the fallback can say what failed only when it sits beside the thing that failed.
- ---
+ **Give the fallback a way back — a reset callback, `resetKeys`, or both.** Without one the only recovery a user has is a full page reload, which costs them everything they had typed.
- **Auto-detection:** error boundary, ErrorBoundary, getDerivedStateFromError, componentDidCatch, fallback UI, react-error-boundary, useErrorBoundary, showBoundary, error recovery, error fallback, onCaughtError, onUncaughtError, onRecoverableError, captureOwnerStack, FallbackProps, resetKeys
+ **Put `role="alert"` on the fallback and make its controls real buttons.** The subtree vanishing is silent otherwise, and a screen reader user gets no announcement that anything went wrong.
- **When to use:**
+ **Route async and event-handler failures through `showBoundary()`.** A boundary cannot see a rejected promise, so an unhandled one leaves the UI showing stale content with no error state at all.
- - 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
+ </critical_requirements>
- **Key patterns covered:**
+ ---
- - Class-based error boundary implementation
- - `react-error-boundary` library patterns (v6+)
- - `useErrorBoundary` hook with `showBoundary()` for async errors
- - Fallback UI with reset functionality and `role="alert"`
- - Strategic boundary placement (granular vs coarse)
- - `resetKeys` for automatic boundary reset
- - **React 19+**: `createRoot` error options for centralized logging
- - **React 19+**: `captureOwnerStack()` for enhanced debugging
+ **Auto-detection:** error boundary, ErrorBoundary, getDerivedStateFromError, componentDidCatch, fallback UI, react-error-boundary, useErrorBoundary, showBoundary, error fallback, onCaughtError, onUncaughtError, onRecoverableError, captureOwnerStack, FallbackProps, resetKeys
- **When NOT to use:**
+ **Applies to:**
- - Event handler errors (use try/catch)
- - Async code errors outside components (use try/catch or showBoundary)
- - Server-side rendering errors (handle at framework level)
- - API request errors (handle in your data fetching layer)
+ - Catching render-phase errors and showing fallback UI in their place
+ - Reset and retry after a caught error, including retry limits and backoff
+ - Deciding where boundaries go and how coarse each one should be
+ - Centralised error reporting from the React root
- **Detailed Resources:**
+ **Handled elsewhere:**
- - [examples/core.md](examples/core.md) - Complete boundary implementations, library usage, granular placement
- - [examples/react-19-hooks.md](examples/react-19-hooks.md) - createRoot error options, captureOwnerStack, error filtering
- - [examples/recovery.md](examples/recovery.md) - Retry limits, exponential backoff, error classification
- - [examples/testing.md](examples/testing.md) - Testing boundaries, async errors, resetKeys
- - [reference.md](reference.md) - Decision frameworks, anti-patterns, checklists
+ - Errors thrown in server rendering — the rendering framework decides what a failed render sends to the client, and no client boundary is mounted yet.
+ - Request failures in a data layer — a boundary sees them only if something rethrows or calls `showBoundary()`; retry and cache invalidation belong to whatever fetches.
+ - Field-level validation feedback — an invalid form field is expected input, so it renders inline rather than replacing the subtree.
+ - The monitoring destination — `onError` and the root handlers hand you an error and a component stack, and where those go is the reporting tool's concern.
---
<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 complement try/catch for imperative code.
+ A render error that no boundary catches unmounts the entire tree: React tears the root down rather
+ than leave a half-rendered document on screen, so one thrown error anywhere becomes a blank page.
- **Core principles:**
+ A boundary buys **isolation** against that: the blast radius is the subtree the nearest boundary
+ wraps, so where the boundaries sit decides how much of the page a single bug costs. That makes
+ placement the real decision — recovery, fallback wording and logging all follow from it.
- 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, accessible 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
+ Boundaries do not replace `try`/`catch`; they cover the one region `try`/`catch` cannot reach, which
+ is React's own render.
</philosophy>
---
<patterns>
- ## Core Patterns
-
- ### Pattern 1: Class-Based Error Boundary (Native React)
-
- Error boundaries MUST be class components -- `getDerivedStateFromError` and `componentDidCatch` have no hook equivalents.
+ ## Core patterns
- #### Two Lifecycle Methods
+ ### Pattern 1: Class-based boundary
- | Method | Phase | Purpose | Side Effects |
- | -------------------------- | ------ | ----------------------------- | ------------ |
- | `getDerivedStateFromError` | Render | Update state to show fallback | NOT allowed |
- | `componentDidCatch` | Commit | Log errors, call callbacks | Allowed |
+ The two lifecycle methods split by phase: `getDerivedStateFromError` is pure and returns state,
+ `componentDidCatch` is where reporting goes.
```typescript
- // ✅ Good - 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;
+ static getDerivedStateFromError(error: Error): State {
+ return { hasError: true, error };
}
- export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
- constructor(props: ErrorBoundaryProps) {
- super(props);
- this.state = { hasError: false, error: null };
- }
-
- static getDerivedStateFromError(error: Error): ErrorBoundaryState {
- return { hasError: true, error };
- }
-
- componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
- 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) {
- if (typeof fallback === "function") return fallback(error, this.handleReset);
- if (fallback) return fallback;
- return (
- <div role="alert">
- <h2>Something went wrong</h2>
- <button onClick={this.handleReset}>Try again</button>
- </div>
- );
- }
- return children;
- }
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
+ this.props.onError?.(error, errorInfo);
}
```
- **Why good:** Render-phase/commit-phase separation, reset capability, flexible fallback API, onError enables logging without coupling to specific tools
-
- ---
-
- ### Pattern 2: react-error-boundary Library (v6+)
-
- Production-ready error boundary with hooks support, resetKeys, and `useErrorBoundary`.
+ Full code: [examples/core.md](examples/core.md)
- ```bash
- npm install react-error-boundary
- ```
+ ### Pattern 2: `react-error-boundary`
- | 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 |
+ A `FallbackComponent` receives `error` and `resetErrorBoundary`, so the retry control lives wherever
+ the design wants it. `onError` keeps reporting out of the fallback.
```typescript
- // ✅ Good - FallbackComponent pattern
- import { ErrorBoundary } from "react-error-boundary";
- import type { FallbackProps } from "react-error-boundary";
+ <ErrorBoundary FallbackComponent={ErrorFallback} onError={report}>
+ <Dashboard />
+ </ErrorBoundary>;
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
- <h2>Something went wrong</h2>
- <pre>{error.message}</pre>
+ <p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
-
- export function App() {
- return (
- <ErrorBoundary
- FallbackComponent={ErrorFallback}
- onError={(error, info) => {
- // Send to your error monitoring service
- console.error("Boundary caught:", error, info);
- }}
- >
- <Dashboard />
- </ErrorBoundary>
- );
- }
```
- **Why good:** Reusable FallbackComponent, onError decouples logging, onReset enables state cleanup
-
- > See [examples/core.md](examples/core.md) for resetKeys, useErrorBoundary, and granular placement examples.
-
- ---
-
- ### Pattern 3: useErrorBoundary Hook (Async Errors)
+ The prop table is in [reference.md](reference.md); full code in [examples/core.md](examples/core.md).
- Error boundaries don't catch async errors. Use `showBoundary()` from `useErrorBoundary` to manually trigger the nearest boundary.
+ ### Pattern 3: `showBoundary()` for async failures
- ```typescript
- // ❌ This async error is NOT caught by error boundary
- async function handleClick() {
- throw new Error("API failed"); // Lost - boundary doesn't see it
- }
- ```
+ An async throw never reaches a boundary on its own. `useErrorBoundary` hands you the trigger.
```typescript
- // ✅ Good - showBoundary propagates async errors
- import { useErrorBoundary } from "react-error-boundary";
-
- function DataLoader() {
- const { showBoundary } = useErrorBoundary();
-
- const handleLoadData = async () => {
- try {
- const response = await fetch("/api/data");
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
- // ... handle success
- } catch (error) {
- showBoundary(error); // Manually trigger nearest boundary
- }
- };
+ const { showBoundary } = useErrorBoundary();
- return <button onClick={handleLoadData}>Load Data</button>;
- }
+ const load = async () => {
+ try {
+ const response = await fetch(endpoint);
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ } catch (error) {
+ showBoundary(error);
+ }
+ };
```
- **Why good:** Propagates async errors to boundary, consistent error UI across sync/async failures
-
- **Use showBoundary for:** Async operations, event handlers, effects that should show fallback UI on failure.
- **Do NOT use for:** Errors handled locally with inline UI, validation errors needing field-level feedback.
+ Use it where the failure should replace the subtree. Leave it alone where the error has inline UI of
+ its own, such as a field-level validation message.
- ---
+ Full code: [examples/core.md](examples/core.md)
- ### Pattern 4: resetKeys for Automatic Reset
+ ### Pattern 4: `resetKeys` for automatic reset
- Use `resetKeys` to auto-reset the boundary when certain values change (e.g., route, selected item).
+ Changing any listed value clears the error and re-renders the children, which is what navigation and
+ record-switching want.
```typescript
- // ✅ Good - Reset boundary on route change
- <ErrorBoundary
- FallbackComponent={ErrorFallback}
- resetKeys={[location.pathname]}
- >
- <Routes />
+ <ErrorBoundary FallbackComponent={ErrorFallback} resetKeys={[currentPath]}>
+ <PageContent path={currentPath} />
</ErrorBoundary>
```
- | Pattern | Use Case |
- | -------------- | --------------------------------- |
- | `[pathname]` | Reset on route change |
- | `[selectedId]` | Reset when viewing different item |
- | `[retryCount]` | Reset after programmatic retry |
-
- **Gotcha:** `resetKeys` comparison is shallow -- objects/arrays need stable references.
-
- ---
-
- ### Pattern 5: Granular Boundary Placement
-
- ```
- App
- ├─ ErrorBoundary (root - last-resort catch-all)
- │ ├─ Header
- │ ├─ ErrorBoundary (sidebar)
- │ │ └─ Sidebar
- │ ├─ ErrorBoundary (main content)
- │ │ ├─ ErrorBoundary (widget A)
- │ │ │ └─ ChartWidget
- │ │ └─ ErrorBoundary (widget B)
- │ │ └─ TableWidget
- │ └─ Footer
- ```
+ Comparison is shallow, so an object or array key needs a stable reference or the boundary resets on
+ every render.
- ```typescript
- // ✅ Good - Granular boundaries isolate failures
- function Dashboard() {
- return (
- <div>
- <ErrorBoundary fallback={<div>Chart unavailable</div>} onError={logError}>
- <ChartWidget />
- </ErrorBoundary>
- <ErrorBoundary fallback={<div>Table unavailable</div>} onError={logError}>
- <DataTable />
- </ErrorBoundary>
- </div>
- );
- }
- ```
+ ### Pattern 5: Granular placement
- **Why good:** One widget failing doesn't crash the dashboard, each feature has contextual fallback
+ Each widget gets its own boundary and its own fallback text; the root boundary catches whatever the
+ inner ones do not.
```typescript
- // ❌ Bad - Single boundary for everything
- <ErrorBoundary fallback={<div>Dashboard error</div>}>
+ <ErrorBoundary fallback={<p>Chart unavailable</p>} onError={report}>
<ChartWidget />
+ </ErrorBoundary>
+ <ErrorBoundary fallback={<p>Table unavailable</p>} onError={report}>
<DataTable />
- <StatsPanel />
</ErrorBoundary>
```
- **Why bad:** One failing widget crashes entire dashboard, users lose access to working features
-
- ---
+ One boundary around all three widgets means the first failure takes the other two with it.
- ### Pattern 6: Fallback UI
+ There is an upper bound: the unit is the feature area a user would recognise as having failed on its
+ own. A boundary around every component adds class components and fallback text nobody reads, and
+ splits one failure into a page of small broken panels rather than one honest message.
- Fallback UI must include `role="alert"` for accessibility, retry button for recovery, and hide error details in production.
+ Full code: [examples/core.md](examples/core.md)
- ```typescript
- // ✅ Good - Environment-aware fallback with accessibility
- function DetailedFallback({ error, resetErrorBoundary }: FallbackProps) {
- const isDev = process.env.NODE_ENV === "development";
- return (
- <div role="alert">
- <h2>Something went wrong</h2>
- {isDev && (
- <details>
- <summary>Error details</summary>
- <pre>{error.message}</pre>
- </details>
- )}
- <button onClick={resetErrorBoundary}>Try again</button>
- <button onClick={() => window.location.reload()}>Refresh page</button>
- </div>
- );
- }
- ```
+ ### Pattern 6: Fallback UI
- **Why good:** `role="alert"` announces to screen readers, dev-only details, multiple recovery options
+ The fallback is UI a user meets at their worst moment: announce it, offer a way out, and keep the
+ stack trace for development.
```typescript
- // ❌ Bad - Missing accessibility, raw errors in production
- <div>
- <pre>{error.stack}</pre>
- <span onClick={reset}>Retry</span> {/* Not keyboard accessible */}
+ <div role="alert">
+ <h2>Something went wrong</h2>
+ {isDevelopment && <pre>{error.message}</pre>}
+ <button onClick={resetErrorBoundary}>Try again</button>
</div>
```
- **Why bad:** No `role="alert"`, exposes internals to users, `span` not keyboard-accessible
-
- ---
+ A `<span onClick>` retry is unreachable by keyboard, and a raw stack in production leaks internals.
- ### Pattern 7: React 19+ createRoot Error Options
+ Full code: [examples/core.md](examples/core.md)
- React 19 adds three root-level error handlers for centralized logging. These complement (not replace) ErrorBoundary components.
+ ### Pattern 7: React 19 root error options
- | Handler | When Called | Use Case |
- | -------------------- | -------------------------------- | ----------------------------------------- |
- | `onCaughtError` | Error caught by an ErrorBoundary | Log handled errors |
- | `onUncaughtError` | Error NOT caught by any boundary | Log fatal errors |
- | `onRecoverableError` | React auto-recovers from error | Log hydration mismatches, suspense errors |
+ `createRoot` takes three handlers that report rather than render, including for errors no boundary
+ caught. They complement boundaries rather than replacing them.
```typescript
- // ✅ Good - Centralized error logging with createRoot
- import { createRoot } from "react-dom/client";
-
- 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: (error, errorInfo) => {
- reportToMonitoring("caught", error, errorInfo.componentStack);
- },
- onUncaughtError: (error, errorInfo) => {
- reportToMonitoring("uncaught", error, errorInfo.componentStack);
- },
- onRecoverableError: (error, errorInfo) => {
- reportToMonitoring("recoverable", error, errorInfo.componentStack);
- },
+ onCaughtError: (error, info) => report("caught", error, info.componentStack),
+ onUncaughtError: (error, info) =>
+ report("uncaught", error, info.componentStack),
+ onRecoverableError: (error, info) =>
+ report("recoverable", error, info.componentStack),
});
- root.render(<App />);
```
- **Why good:** Single configuration point for all React error logging, catches errors that escape all boundaries
-
- > See [examples/react-19-hooks.md](examples/react-19-hooks.md) for `captureOwnerStack()`, error filtering, and hydrateRoot patterns.
+ Full code: [examples/react-19-hooks.md](examples/react-19-hooks.md)
</patterns>
---
<red_flags>
- ## RED FLAGS
-
- **High Priority:**
-
- - Missing error boundaries entirely -- app crashes on any render error
- - Single root boundary only -- no isolation between features
- - No reset/retry functionality -- users must refresh page
- - Missing `role="alert"` on fallback -- screen readers don't announce errors
- - Side effects in `getDerivedStateFromError` -- violates React phase rules
+ ## Red flags
- **Medium Priority:**
+ **Breaks at runtime:**
- - Not using `showBoundary()` for async errors -- they silently fail
- - Same fallback for all boundaries -- no context about what failed
- - No `onError` callback -- errors not reported to monitoring
- - Overly granular boundaries (every component) -- unnecessary overhead
+ - Side effects in `getDerivedStateFromError` — it runs during render, so a fetch or a `setState` there breaks React's phase rules — report from `componentDidCatch` instead.
+ - A boundary wrapping itself — a boundary never catches its own render error, and the throw escapes to the parent boundary or to the root — keep the fallback trivial.
+ - An unstable `resetKeys` entry — the shallow compare sees a new array or object every render and resets the boundary continuously — memoise the value or key on a primitive.
+ - `<span onClick={reset}>` as the retry control — not focusable and not activated by Enter or Space — use `<button>`.
- **Gotchas & Edge Cases:**
+ **Surprising behaviour:**
- - `getDerivedStateFromError` runs during render -- no side effects allowed
- - Error boundaries don't catch errors in **themselves** -- only children
- - Nested boundaries: **innermost** boundary catches first
- - Hot reload can trigger boundaries in development (expected behavior)
- - `resetKeys` comparison is shallow -- objects/arrays need stable references
- - SSR hydration errors may not be caught by client-side boundaries
- - **React 19:** `captureOwnerStack()` returns `null` in production
- - **React 19:** `onCaughtError` runs AFTER boundary's `componentDidCatch`, not before
- - **React 19:** `onRecoverableError` may have `error.cause` with the original thrown error
- - **React 19:** These options are silently ignored on React 18
+ - Async and event-handler throws never reach a boundary; without `showBoundary()` they vanish.
+ - The innermost boundary wins, so a wide root fallback appears only when every inner one was missed.
+ - SSR hydration errors surface as recoverable rather than caught, and a client boundary may never see them.
+ - Development hot reload trips boundaries that production never would.
+ - `onCaughtError` runs after the boundary's own `componentDidCatch`, not before it.
+ - `onRecoverableError` often carries the original throw on `error.cause`.
+ - `captureOwnerStack()` returns `null` outside development.
+ - The three root options are silently ignored on React 18 — no warning, no error, no logging.
</red_flags>
-
- ---
-
- <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)**
-
- **Failure to follow these rules will result in poor error handling, inaccessible UIs, or unrecoverable error states.**
-
- </critical_reminders>