
Web Error Handling Error Boundaries
- 42 installs
- 19 repo stars
- Updated July 19, 2026
- agents-inc/skills
web-error-handling-error-boundaries is a Claude Code skill that generates React error boundary patterns with fallback UI, reset/retry, and React 19 createRoot error hooks.
About
A Claude Code skill for React error boundaries that catch render errors and show fallback UI. It covers class-based boundaries, the react-error-boundary library, the useErrorBoundary/showBoundary hook for async errors, resetKeys, and React 19 createRoot error options. A developer uses it to isolate feature failures and add retry so a component error does not crash the whole app.
- React error boundary patterns with fallback UI and reset/retry
- react-error-boundary v6+ and showBoundary for async errors
- React 19 createRoot error hooks for centralized logging
Web Error Handling Error Boundaries by the numbers
- 42 all-time installs (skills.sh)
- Ranked #1,360 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
web-error-handling-error-boundaries capabilities & compatibility
- Capabilities
- error handling · fallback ui · error recovery · react resilience
- Use cases
- frontend · debugging
What web-error-handling-error-boundaries says it does
Error boundaries catch JavaScript errors in component trees and display fallback UI.
Boundaries do NOT catch event handler, async, or SSR errors -- use `showBoundary()` hook for async.
npx skills add https://github.com/agents-inc/skills --skill web-error-handling-error-boundariesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 42 |
|---|---|
| repo stars | ★ 19 |
| Last updated | July 19, 2026 |
| Repository | agents-inc/skills ↗ |
What it does
Add React error boundaries with fallback UI and retry to isolate component failures.
Who is it for?
Isolating feature failures and adding retry in a React app
Skip if: Event handler, async-outside-component, or SSR errors
When should I use this skill?
Wrapping React features in error boundaries with fallback UI
What you get
Isolated error boundaries with accessible fallback UI and retry
- error boundary components
- react-error-boundary fallback UI
- resetKeys/retry logic
By the numbers
- 4+ resource files (core, react-19-hooks, recovery, testing)
Files
React Error Boundaries
Quick Guide: Error boundaries catch JavaScript errors in component trees and display fallback UI. Usereact-error-boundarylibrary (v6+) for production apps. Place boundaries strategically around features, not just root. Boundaries do NOT catch event handler, async, or SSR errors -- useshowBoundary()hook for async. React 19+: UsecreateRootoptions (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)
</critical_requirements>
---
Auto-detection: error boundary, ErrorBoundary, getDerivedStateFromError, componentDidCatch, fallback UI, react-error-boundary, useErrorBoundary, showBoundary, error recovery, error fallback, onCaughtError, onUncaughtError, onRecoverableError, captureOwnerStack, FallbackProps, resetKeys
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-boundarylibrary patterns (v6+)useErrorBoundaryhook withshowBoundary()for async errors- Fallback UI with reset functionality and
role="alert" - Strategic boundary placement (granular vs coarse)
resetKeysfor automatic boundary reset- React 19+:
createRooterror options for centralized logging - React 19+:
captureOwnerStack()for enhanced debugging
When NOT to use:
- 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)
Detailed Resources:
- examples/core.md - Complete boundary implementations, library usage, granular placement
- examples/react-19-hooks.md - createRoot error options, captureOwnerStack, error filtering
- examples/recovery.md - Retry limits, exponential backoff, error classification
- examples/testing.md - Testing boundaries, async errors, resetKeys
- reference.md - Decision frameworks, anti-patterns, checklists
---
<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.
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, 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
</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.
Two Lifecycle Methods
| Method | Phase | Purpose | Side Effects |
|---|---|---|---|
getDerivedStateFromError | Render | Update state to show fallback | NOT allowed |
componentDidCatch | Commit | Log errors, call callbacks | Allowed |
// ✅ 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;
}
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;
}
}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.
npm install 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 |
// ✅ Good - FallbackComponent pattern
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>
);
}
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 for resetKeys, useErrorBoundary, and granular placement examples.
---
Pattern 3: useErrorBoundary Hook (Async Errors)
Error boundaries don't catch async errors. Use showBoundary() from useErrorBoundary to manually trigger the nearest boundary.
// ❌ This async error is NOT caught by error boundary
async function handleClick() {
throw new Error("API failed"); // Lost - boundary doesn't see it
}// ✅ 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
}
};
return <button onClick={handleLoadData}>Load Data</button>;
}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.
---
Pattern 4: resetKeys for Automatic Reset
Use resetKeys to auto-reset the boundary when certain values change (e.g., route, selected item).
// ✅ Good - Reset boundary on route change
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[location.pathname]}
>
<Routes />
</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// ✅ 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>
);
}Why good: One widget failing doesn't crash the dashboard, each feature has contextual fallback
// ❌ Bad - Single boundary for everything
<ErrorBoundary fallback={<div>Dashboard error</div>}>
<ChartWidget />
<DataTable />
<StatsPanel />
</ErrorBoundary>Why bad: One failing widget crashes entire dashboard, users lose access to working features
---
Pattern 6: Fallback UI
Fallback UI must include role="alert" for accessibility, retry button for recovery, and hide error details in production.
// ✅ 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>
);
}Why good: role="alert" announces to screen readers, dev-only details, multiple recovery options
// ❌ Bad - Missing accessibility, raw errors in production
<div>
<pre>{error.stack}</pre>
<span onClick={reset}>Retry</span> {/* Not keyboard accessible */}
</div>Why bad: No role="alert", exposes internals to users, span not keyboard-accessible
---
Pattern 7: React 19+ createRoot Error Options
React 19 adds three root-level error handlers for centralized logging. These complement (not replace) ErrorBoundary components.
| 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 |
// ✅ 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);
},
});
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 for captureOwnerStack(), error filtering, and hydrateRoot patterns.</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
Medium Priority:
- Not using
showBoundary()for async errors -- they silently fail - Same fallback for all boundaries -- no context about what failed
- No
onErrorcallback -- errors not reported to monitoring - Overly granular boundaries (every component) -- unnecessary overhead
Gotchas & Edge Cases:
getDerivedStateFromErrorruns 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)
resetKeyscomparison is shallow -- objects/arrays need stable references- SSR hydration errors may not be caught by client-side boundaries
- React 19:
captureOwnerStack()returnsnullin production - React 19:
onCaughtErrorruns AFTER boundary'scomponentDidCatch, not before - React 19:
onRecoverableErrormay haveerror.causewith the original thrown error - React 19: These options are silently ignored on React 18
</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>
Error Boundaries - Core Examples
Complete code examples for error boundary patterns. See SKILL.md for core concepts.
Extended Examples:
- react-19-hooks.md - React 19+ createRoot error options,
captureOwnerStack(), error filtering - recovery.md - Retry limits, exponential backoff, error classification
- testing.md - Testing boundaries, async errors, resetKeys
---
Pattern 1: Class-Based Error Boundary
Good Example - Complete Error Boundary Implementation
// src/components/error-boundary/error-boundary.tsx
import { Component } from "react";
import type { ErrorInfo, ReactNode } from "react";
export interface ErrorBoundaryProps {
children: ReactNode;
/** Static fallback UI */
fallback?: ReactNode;
/** Function that receives error and reset function */
fallbackRender?: (props: { error: Error; resetErrorBoundary: () => void }) => ReactNode;
/** Called when error is caught */
onError?: (error: Error, errorInfo: ErrorInfo) => void;
/** Called before boundary resets */
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 };
}
// Render phase - update state to show fallback
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
// Commit phase - side effects allowed
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// Call error callback for logging
this.props.onError?.(error, errorInfo);
}
resetErrorBoundary = (): void => {
this.props.onReset?.();
this.setState({ hasError: false, error: null });
};
render(): ReactNode {
const { hasError, error } = this.state;
const { children, fallback, fallbackRender } = this.props;
if (hasError && error) {
// Priority: fallbackRender > fallback > default
if (fallbackRender) {
return fallbackRender({
error,
resetErrorBoundary: this.resetErrorBoundary,
});
}
if (fallback) {
return fallback;
}
// Default fallback
return (
<div role="alert" style={{ padding: "1rem", textAlign: "center" }}>
<h2>Something went wrong</h2>
<pre style={{ color: "red", whiteSpace: "pre-wrap" }}>
{error.message}
</pre>
<button
onClick={this.resetErrorBoundary}
style={{ marginTop: "1rem", padding: "0.5rem 1rem" }}
>
Try again
</button>
</div>
);
}
return children;
}
}Why good: Complete implementation with all common features, supports multiple fallback patterns, proper lifecycle method separation, TypeScript types exported for consumers
---
Pattern 2: react-error-boundary Library Usage
Good Example - Basic Usage with FallbackComponent
// src/app.tsx
import { ErrorBoundary } from "react-error-boundary";
import type { FallbackProps } from "react-error-boundary";
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert" style={{ padding: "2rem", textAlign: "center" }}>
<h2>Something went wrong</h2>
<pre style={{ color: "red", whiteSpace: "pre-wrap" }}>
{error.message}
</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function logError(error: Error, info: { componentStack?: string | null }) {
// Send to your error tracking service
console.error("Error boundary caught:", error);
console.error("Component stack:", info.componentStack);
}
export function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={logError}
onReset={() => {
// Reset application state if needed
}}
>
<MainContent />
</ErrorBoundary>
);
}Why good: Uses typed FallbackProps from library, separates error logging into dedicated function, provides reset capability
---
Good Example - Using resetKeys for Automatic Reset
// src/features/user-profile/user-profile.tsx
import { ErrorBoundary } from "react-error-boundary";
interface UserProfileProps {
userId: string;
}
export function UserProfile({ userId }: UserProfileProps) {
return (
<ErrorBoundary
FallbackComponent={ProfileErrorFallback}
resetKeys={[userId]}
>
<ProfileContent userId={userId} />
</ErrorBoundary>
);
}
function ProfileErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<p>Failed to load profile</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
}Why good: Boundary automatically resets when userId changes, prevents stale error state when navigating between users
---
Good Example - useErrorBoundary Hook for Async Errors
// src/features/data-loader/data-loader.tsx
import { useState } from "react";
import { useErrorBoundary, ErrorBoundary } from "react-error-boundary";
import type { FallbackProps } from "react-error-boundary";
const API_ENDPOINT = "/api/items";
interface Item {
id: string;
name: string;
}
function DataLoaderContent() {
const { showBoundary } = useErrorBoundary();
const [items, setItems] = useState<Item[]>([]);
const [isLoading, setIsLoading] = useState(false);
const handleLoadData = async () => {
setIsLoading(true);
try {
const response = await fetch(API_ENDPOINT);
if (!response.ok) {
throw new Error(`Failed to fetch: HTTP ${response.status}`);
}
const data = await response.json();
setItems(data);
} catch (error) {
// Propagate to nearest error boundary
showBoundary(error);
} finally {
setIsLoading(false);
}
};
return (
<div>
<button onClick={handleLoadData} disabled={isLoading}>
{isLoading ? "Loading..." : "Load Data"}
</button>
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
function DataLoaderFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<p>Failed to load data: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
export function DataLoader() {
return (
<ErrorBoundary FallbackComponent={DataLoaderFallback}>
<DataLoaderContent />
</ErrorBoundary>
);
}Why good: useErrorBoundary hook propagates async errors to boundary, consistent error UI for both sync and async failures, named constant for API endpoint
---
Pattern 3: Granular Boundary Placement
Good Example - Dashboard with Isolated Widgets
// src/features/dashboard/multi-boundary-dashboard.tsx
import { ErrorBoundary } from "react-error-boundary";
import type { FallbackProps } from "react-error-boundary";
function WidgetFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert" className="widget-error">
<p>Widget unavailable</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
}
function logWidgetError(widgetName: string) {
return (error: Error, info: { componentStack?: string | null }) => {
// Log with widget context
console.error(`${widgetName} error:`, error, info.componentStack);
};
}
export function Dashboard() {
return (
<div className="dashboard">
<header>
<h1>Dashboard</h1>
</header>
<div className="widgets-grid">
<ErrorBoundary
FallbackComponent={WidgetFallback}
onError={logWidgetError("RevenueChart")}
>
<RevenueChart />
</ErrorBoundary>
<ErrorBoundary
FallbackComponent={WidgetFallback}
onError={logWidgetError("UserStats")}
>
<UserStats />
</ErrorBoundary>
<ErrorBoundary
FallbackComponent={WidgetFallback}
onError={logWidgetError("ActivityFeed")}
>
<ActivityFeed />
</ErrorBoundary>
<ErrorBoundary
FallbackComponent={WidgetFallback}
onError={logWidgetError("RecentOrders")}
>
<RecentOrders />
</ErrorBoundary>
</div>
</div>
);
}Why good: Each widget has isolated failure domain, reusable fallback component, error logging includes widget context
---
Pattern 4: Minimal Accessible Fallback
Good Example - Basic Accessible Fallback
// src/components/fallbacks/minimal-fallback.tsx
import type { FallbackProps } from "react-error-boundary";
export function MinimalFallback({ resetErrorBoundary }: FallbackProps) {
return (
<div role="alert" aria-live="assertive">
<p>Failed to load content</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}Why good: Minimal but functional, role="alert" announces to screen readers, aria-live ensures immediate announcement
---
Anti-Pattern Examples
Bad Example - Single Root Boundary Only
// BAD: Single boundary for entire app
function App() {
return (
<ErrorBoundary fallback={<div>App crashed</div>}>
<Header />
<Sidebar />
<MainContent />
<Dashboard />
<Footer />
</ErrorBoundary>
);
}Why bad: One error anywhere crashes the entire app, no isolation between features, poor user experience
---
Bad Example - No Retry Functionality
// BAD: No way to recover
function BadFallback() {
return <div>Error occurred. Please refresh the page.</div>;
}Why bad: Forces full page refresh to recover, loses user state, poor UX for transient errors
---
Bad Example - Missing Accessibility
// BAD: No accessibility attributes
function InaccessibleFallback() {
return (
<div>
<p>Something went wrong</p>
<span onClick={reset}>Click to retry</span>
</div>
);
}Why bad: No role="alert" means screen readers don't announce error, span with onClick is not keyboard accessible, screen reader users don't know an error occurred
---
See also:
>
- react-19-hooks.md - React 19+ createRoot error options, captureOwnerStack
- recovery.md - Retry limits, exponential backoff, error classification
- testing.md - Testing error boundary behavior
Error Boundaries - React 19+ Error Hooks
React 19 error handling patterns withcreateRootoptions andcaptureOwnerStack(). See core.md for basic error boundary patterns.
Prerequisites: Understand basic error boundaries from core.md first. These patterns complement, not replace, error boundaries.
React Version: These patterns require React 19+.
---
Pattern 1: createRoot Error Options
React 19 introduces three error handler options for createRoot and hydrateRoot. These provide centralized error logging separate from error boundary fallback UI.
Three Error Handlers Explained
| Handler | Triggered When | Typical Action |
|---|---|---|
onCaughtError | Error caught by an ErrorBoundary | Log to monitoring, handled gracefully |
onUncaughtError | Error NOT caught by any boundary | Log as fatal, show error overlay |
onRecoverableError | React auto-recovers (hydration mismatch, suspended promise rejection) | Log warning, usually non-critical |
Good Example - Complete Setup
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./app";
const ROOT_ELEMENT_ID = "root";
// Error logging function
function reportError(
type: "caught" | "uncaught" | "recoverable",
error: Error,
errorInfo: { componentStack?: string | null }
) {
const payload = {
type,
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
url: window.location.href,
};
// Send to your error tracking service
if (process.env.NODE_ENV === "production") {
fetch("/api/errors", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}).catch(() => {
// Fail silently - don't let error reporting cause more errors
});
} else {
console.group(`React ${type} error`);
console.error("Error:", error);
console.error("Component Stack:", errorInfo.componentStack);
console.groupEnd();
}
}
const container = document.getElementById(ROOT_ELEMENT_ID);
if (!container) {
throw new Error(`Root element #${ROOT_ELEMENT_ID} not found`);
}
const root = createRoot(container, {
// Called when an ErrorBoundary catches an error
onCaughtError: (error, errorInfo) => {
reportError("caught", error, errorInfo);
},
// Called when an error is NOT caught by any boundary
onUncaughtError: (error, errorInfo) => {
reportError("uncaught", error, errorInfo);
},
// Called when React auto-recovers from an error
onRecoverableError: (error, errorInfo) => {
reportError("recoverable", error, errorInfo);
},
});
root.render(
<StrictMode>
<App />
</StrictMode>
);Why good: Centralized error reporting for all error types, different handling per error severity, production-safe with silent fallback for reporting failures, development-friendly console grouping
---
Pattern 2: Integration with Error Monitoring Services
Most error monitoring SDKs provide React-specific handlers that wrap createRoot error options. Check your monitoring service's documentation for React 19 integration helpers.
Good Example - Generic Monitoring Integration
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./app";
const ROOT_ELEMENT_ID = "root";
// Your error monitoring service's React handler
// Most monitoring SDKs provide a reactErrorHandler() wrapper
function createMonitoringHandler(
level: "caught" | "uncaught" | "recoverable",
) {
return (error: Error, errorInfo: { componentStack?: string | null }) => {
// Send to your monitoring service with appropriate severity
reportToMonitoring({
level,
error,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
});
};
}
const container = document.getElementById(ROOT_ELEMENT_ID);
if (!container) throw new Error("Root element not found");
const root = createRoot(container, {
onCaughtError: createMonitoringHandler("caught"),
onUncaughtError: createMonitoringHandler("uncaught"),
onRecoverableError: createMonitoringHandler("recoverable"),
});
root.render(
<StrictMode>
<App />
</StrictMode>
);Why good: Centralized error reporting through monitoring service, severity-aware handling, works with any monitoring SDK that accepts error + component stack
---
Pattern 3: captureOwnerStack() for Enhanced Debugging
React 19 provides captureOwnerStack() to capture the "owner stack" - the chain of components that created the current element. Available only in development.
Owner Stack vs Component Stack
| Stack Type | What It Shows | Example |
|---|---|---|
| Component Stack | All components in the tree | App > Layout > Page > ErrorBoundary > Widget |
| Owner Stack | Components that "created" the element | App > Page > Widget (skips pass-through components) |
Good Example - Custom Console Error Overlay
// src/utils/error-overlay.ts
import * as React from "react";
interface ErrorOverlayEntry {
message: string;
componentStack?: string | null;
ownerStack?: string | null;
timestamp: string;
}
const errorLog: ErrorOverlayEntry[] = [];
// Only available in development
function getOwnerStack(): string | null {
if (process.env.NODE_ENV !== "production") {
return React.captureOwnerStack?.() ?? null;
}
return null;
}
// Patch console.error to capture owner stacks
const originalConsoleError = console.error;
console.error = function patchedConsoleError(...args: unknown[]) {
originalConsoleError.apply(console, args);
const ownerStack = getOwnerStack();
if (ownerStack) {
errorLog.push({
message: String(args[0]),
ownerStack,
timestamp: new Date().toISOString(),
});
}
};
export function getErrorLog(): readonly ErrorOverlayEntry[] {
return errorLog;
}
export function clearErrorLog(): void {
errorLog.length = 0;
}Why good: Owner stack shows which component CREATED the error, not just where it appeared, invaluable for debugging prop-drilling issues, only runs in development (no production overhead)
Good Example - Using captureOwnerStack in Error Handlers
// src/main.tsx
import * as React from "react";
import { createRoot } from "react-dom/client";
const ROOT_ELEMENT_ID = "root";
function enhancedErrorLog(
error: Error,
errorInfo: { componentStack?: string | null },
) {
const ownerStack =
process.env.NODE_ENV !== "production" ? React.captureOwnerStack?.() : null;
console.group("React Error Report");
console.error("Error:", error.message);
console.error("Component Stack:", errorInfo.componentStack);
if (ownerStack) {
console.error("Owner Stack:", ownerStack);
}
console.groupEnd();
}
const container = document.getElementById(ROOT_ELEMENT_ID);
if (!container) throw new Error("Root element not found");
const root = createRoot(container, {
onCaughtError: enhancedErrorLog,
onUncaughtError: enhancedErrorLog,
onRecoverableError: enhancedErrorLog,
});Why good: Combines both stacks for complete debugging picture, conditionally includes owner stack only in development, structured console output for easy reading
---
Pattern 4: Filtering Known Errors
Sometimes you want to ignore certain expected errors in your error handlers.
Good Example - Error Filtering
// src/main.tsx
import { createRoot } from "react-dom/client";
const ROOT_ELEMENT_ID = "root";
// Errors to ignore (expected, handled elsewhere)
const IGNORED_ERROR_MESSAGES = [
"ResizeObserver loop limit exceeded",
"ResizeObserver loop completed with undelivered notifications",
"Network request failed", // Handled by data fetching layer
] as const;
function shouldIgnoreError(error: Error): boolean {
return IGNORED_ERROR_MESSAGES.some((msg) => error.message.includes(msg));
}
const container = document.getElementById(ROOT_ELEMENT_ID);
if (!container) throw new Error("Root element not found");
const root = createRoot(container, {
onCaughtError: (error, errorInfo) => {
if (shouldIgnoreError(error)) return;
// Log to monitoring
reportToMonitoring("caught", error, errorInfo);
},
onUncaughtError: (error, errorInfo) => {
// Never ignore uncaught errors - they're fatal
reportToMonitoring("uncaught", error, errorInfo);
},
onRecoverableError: (error, errorInfo) => {
if (shouldIgnoreError(error)) return;
// Log at warning level
reportToMonitoring("recoverable", error, errorInfo);
},
});Why good: Reduces noise from known browser quirks (ResizeObserver), keeps monitoring focused on actionable errors, named constant for ignored messages
---
Pattern 5: Combining with ErrorBoundary Components
The createRoot handlers and ErrorBoundary components serve different purposes and should be used together.
Architecture
createRoot({
onCaughtError, ← Logging/monitoring (all caught errors)
onUncaughtError, ← Fatal error handling
onRecoverableError
})
│
└── App
└── ErrorBoundary ← User-facing fallback UI
└── Feature
└── ErrorBoundary ← Feature-specific fallback
└── WidgetGood Example - Complete Error Strategy
// src/main.tsx
import { createRoot } from "react-dom/client";
import { ErrorBoundary } from "react-error-boundary";
import type { FallbackProps } from "react-error-boundary";
const ROOT_ELEMENT_ID = "root";
// Root-level logging (separate from UI)
const root = createRoot(container, {
onCaughtError: (error) => sendToMonitoring(error),
onUncaughtError: (error) => sendToMonitoring(error),
onRecoverableError: (error) => sendToMonitoring(error),
});
// App-level fallback for fatal errors
function AppFallback() {
return (
<div role="alert">
<h1>Application Error</h1>
<p>Please refresh the page.</p>
<button onClick={() => window.location.reload()}>Refresh</button>
</div>
);
}
// Feature-level fallback with retry
function FeatureFallback({ resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<p>Feature unavailable</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
}
// App structure
function App() {
return (
<ErrorBoundary FallbackComponent={AppFallback}>
<Layout>
<ErrorBoundary FallbackComponent={FeatureFallback}>
<Dashboard />
</ErrorBoundary>
</Layout>
</ErrorBoundary>
);
}
root.render(<App />);Why good: Separation of concerns - root handlers for logging, boundaries for UI, granular fallbacks at different levels, both systems work together
---
Pattern 6: hydrateRoot Error Options
For server-side rendered apps, hydrateRoot accepts the same error options.
Good Example - SSR Hydration with Error Handling
// src/entry-client.tsx
import { hydrateRoot } from "react-dom/client";
import { App } from "./app";
const ROOT_ELEMENT_ID = "root";
const HYDRATION_MISMATCH_THRESHOLD = 5;
let hydrationMismatchCount = 0;
const container = document.getElementById(ROOT_ELEMENT_ID);
if (!container) throw new Error("Root element not found");
hydrateRoot(container, <App />, {
onCaughtError: (error, errorInfo) => {
reportToMonitoring("caught", error, errorInfo);
},
onUncaughtError: (error, errorInfo) => {
reportToMonitoring("uncaught", error, errorInfo);
},
onRecoverableError: (error, errorInfo) => {
// Track hydration mismatches specifically
if (error.message.includes("Hydration")) {
hydrationMismatchCount++;
console.warn(`Hydration mismatch #${hydrationMismatchCount}`);
// Alert if too many mismatches (indicates SSR/client mismatch)
if (hydrationMismatchCount >= HYDRATION_MISMATCH_THRESHOLD) {
reportToMonitoring("hydration-critical", error, {
...errorInfo,
mismatchCount: hydrationMismatchCount,
});
}
} else {
reportToMonitoring("recoverable", error, errorInfo);
}
},
});Why good: Same API as createRoot, specific handling for hydration mismatches, threshold-based alerting prevents noise while catching systematic issues
---
When to Use Each
Decision Framework
Need to LOG/REPORT errors?
├─ YES → Use createRoot options (onCaughtError, onUncaughtError, onRecoverableError)
└─ NO → Skip createRoot options
Need to DISPLAY fallback UI?
├─ YES → Use ErrorBoundary components
└─ NO → Skip ErrorBoundary
Error type?
├─ Render/lifecycle error → ErrorBoundary catches, onCaughtError logs
├─ Async error (fetch, event handler) → useErrorBoundary + showBoundary()
├─ Fatal (no boundary catches) → onUncaughtError logs
└─ Hydration/recovery → onRecoverableError logsSummary Table
| Scenario | ErrorBoundary | createRoot Options |
|---|---|---|
| Show fallback UI | ✅ Required | Not involved |
| Log caught errors | Optional onError | ✅ onCaughtError |
| Log uncaught errors | Cannot catch | ✅ onUncaughtError |
| Track hydration issues | Cannot catch | ✅ onRecoverableError |
---
Gotchas
1. `captureOwnerStack()` is development-only - Returns null in production, always check process.env.NODE_ENV
2. Error handlers run AFTER boundary catches - onCaughtError runs after the boundary's componentDidCatch, not before
3. React 19 consolidates error messages - Instead of duplicate console.error calls, React 19 logs a single error message with all relevant info
4. `onRecoverableError` includes original error - The error parameter may have error.cause containing the original thrown error
5. SSR hydration errors are "recoverable" - They trigger onRecoverableError, not onUncaughtError
6. These options don't exist on React 18 - Check React version before using, or the options will be silently ignored
---
See also:
>
- core.md - Basic error boundary patterns
- recovery.md - Retry limits and error classification
- testing.md - Testing error boundaries
Error Boundaries - Recovery Patterns
Advanced recovery patterns including retry limits and error classification. See core.md for basic patterns.
Prerequisites: Understand the basic ErrorBoundary component and reset functionality from core examples first.
---
Pattern 7: Retry Limit Boundaries
Good Example - Retry Tracking Boundary
// src/components/error-boundary/retry-limited-boundary.tsx
import { useState, useCallback, type ReactNode } from "react";
import { ErrorBoundary } from "react-error-boundary";
import type { FallbackProps } from "react-error-boundary";
const MAX_RETRY_COUNT = 3;
interface RetryLimitedBoundaryProps {
children: ReactNode;
maxRetries?: number;
onMaxRetriesReached?: (error: Error) => void;
}
export function RetryLimitedBoundary({
children,
maxRetries = MAX_RETRY_COUNT,
onMaxRetriesReached,
}: RetryLimitedBoundaryProps) {
const [retryCount, setRetryCount] = useState(0);
const [lastError, setLastError] = useState<Error | null>(null);
const handleError = useCallback((error: Error) => {
setLastError(error);
}, []);
const handleReset = useCallback(() => {
const newCount = retryCount + 1;
setRetryCount(newCount);
if (newCount >= maxRetries && lastError) {
onMaxRetriesReached?.(lastError);
}
}, [retryCount, maxRetries, lastError, onMaxRetriesReached]);
if (retryCount >= maxRetries) {
return (
<div role="alert">
<h3>Unable to load</h3>
<p>We tried {maxRetries} times but couldn't load this content.</p>
<p>Please refresh the page or try again later.</p>
<button onClick={() => window.location.reload()}>Refresh page</button>
</div>
);
}
return (
<ErrorBoundary
onError={handleError}
onReset={handleReset}
fallbackRender={({ error, resetErrorBoundary }: FallbackProps) => (
<div role="alert">
<p>Something went wrong</p>
<p>Attempt {retryCount + 1} of {maxRetries}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
{children}
</ErrorBoundary>
);
}Why good: Prevents infinite retry loops, provides escalation path after max retries, tracks retry count for user feedback
---
Good Example - Exponential Backoff Retry
// src/components/error-boundary/backoff-retry-boundary.tsx
import { useState, useCallback, useRef, type ReactNode } from "react";
import { ErrorBoundary } from "react-error-boundary";
import type { FallbackProps } from "react-error-boundary";
const BASE_DELAY_MS = 1000;
const MAX_DELAY_MS = 30000;
const MAX_RETRIES = 5;
interface BackoffRetryBoundaryProps {
children: ReactNode;
maxRetries?: number;
baseDelayMs?: number;
maxDelayMs?: number;
}
export function BackoffRetryBoundary({
children,
maxRetries = MAX_RETRIES,
baseDelayMs = BASE_DELAY_MS,
maxDelayMs = MAX_DELAY_MS,
}: BackoffRetryBoundaryProps) {
const [retryCount, setRetryCount] = useState(0);
const [isWaiting, setIsWaiting] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout>();
const calculateDelay = useCallback(
(attempt: number): number => {
const delay = baseDelayMs * Math.pow(2, attempt);
return Math.min(delay, maxDelayMs);
},
[baseDelayMs, maxDelayMs]
);
const handleRetry = useCallback(
(resetErrorBoundary: () => void) => {
const delay = calculateDelay(retryCount);
setIsWaiting(true);
timeoutRef.current = setTimeout(() => {
setIsWaiting(false);
setRetryCount((prev) => prev + 1);
resetErrorBoundary();
}, delay);
},
[retryCount, calculateDelay]
);
if (retryCount >= maxRetries) {
return (
<div role="alert">
<h3>Maximum retries reached</h3>
<p>Please try again later or contact support.</p>
<button onClick={() => window.location.reload()}>Refresh page</button>
</div>
);
}
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }: FallbackProps) => {
const nextDelay = calculateDelay(retryCount);
const nextDelaySeconds = Math.round(nextDelay / 1000);
return (
<div role="alert">
<p>Something went wrong: {error.message}</p>
<p>Retry {retryCount + 1} of {maxRetries}</p>
{isWaiting ? (
<p>Retrying in {nextDelaySeconds} seconds...</p>
) : (
<button onClick={() => handleRetry(resetErrorBoundary)}>
Retry (wait {nextDelaySeconds}s)
</button>
)}
</div>
);
}}
>
{children}
</ErrorBoundary>
);
}Why good: Exponential backoff prevents server overload, shows wait time to user, respects max delay cap
---
Pattern 8: Error Classification
Good Example - Classified Error Handling
// src/components/error-boundary/classified-boundary.tsx
import { ErrorBoundary } from "react-error-boundary";
import type { ReactNode } from "react";
import type { FallbackProps } from "react-error-boundary";
// Error classification types
type ErrorCategory = "network" | "auth" | "validation" | "unknown";
interface ClassifiedError extends Error {
category?: ErrorCategory;
retryable?: boolean;
}
function classifyError(error: Error): ClassifiedError {
const classified = error as ClassifiedError;
// Classify based on error characteristics
if (error.message.includes("fetch") || error.message.includes("network")) {
classified.category = "network";
classified.retryable = true;
} else if (error.message.includes("401") || error.message.includes("403")) {
classified.category = "auth";
classified.retryable = false;
} else if (error.message.includes("validation")) {
classified.category = "validation";
classified.retryable = false;
} else {
classified.category = "unknown";
classified.retryable = true;
}
return classified;
}
function ClassifiedFallback({ error, resetErrorBoundary }: FallbackProps) {
const classified = classifyError(error);
switch (classified.category) {
case "network":
return (
<div role="alert">
<h3>Connection Issue</h3>
<p>Please check your internet connection.</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
case "auth":
return (
<div role="alert">
<h3>Session Expired</h3>
<p>Please log in again to continue.</p>
<button onClick={() => window.location.href = "/login"}>
Go to Login
</button>
</div>
);
case "validation":
return (
<div role="alert">
<h3>Invalid Data</h3>
<p>There was a problem with the data. Please try again.</p>
</div>
);
default:
return (
<div role="alert">
<h3>Something went wrong</h3>
<p>An unexpected error occurred.</p>
{classified.retryable && (
<button onClick={resetErrorBoundary}>Try again</button>
)}
</div>
);
}
}
interface ClassifiedBoundaryProps {
children: ReactNode;
onError?: (error: ClassifiedError) => void;
}
export function ClassifiedBoundary({ children, onError }: ClassifiedBoundaryProps) {
const handleError = (error: Error) => {
const classified = classifyError(error);
onError?.(classified);
};
return (
<ErrorBoundary
FallbackComponent={ClassifiedFallback}
onError={handleError}
>
{children}
</ErrorBoundary>
);
}Why good: Different recovery actions based on error type, user-friendly messages per category, programmatic error classification
---
Good Example - HTTP Status-Based Classification
// src/utils/error-classification.ts
interface HttpError extends Error {
status: number;
statusText: string;
}
export function isHttpError(error: Error): error is HttpError {
return "status" in error && typeof (error as HttpError).status === "number";
}
export type HttpErrorCategory =
| "client-error" // 4xx
| "server-error" // 5xx
| "network-error" // No response
| "timeout-error"; // Request timeout
export interface CategorizedHttpError {
category: HttpErrorCategory;
retryable: boolean;
userMessage: string;
technicalDetails: string;
}
const RETRY_DELAY_MS = 5000;
export function categorizeHttpError(error: Error): CategorizedHttpError {
if (!isHttpError(error)) {
// Network error - no response received
if (
error.message.includes("NetworkError") ||
error.message.includes("fetch")
) {
return {
category: "network-error",
retryable: true,
userMessage:
"Unable to connect. Please check your internet connection.",
technicalDetails: error.message,
};
}
// Timeout
if (error.message.includes("timeout") || error.name === "AbortError") {
return {
category: "timeout-error",
retryable: true,
userMessage: "Request timed out. Please try again.",
technicalDetails: error.message,
};
}
// Unknown error
return {
category: "client-error",
retryable: false,
userMessage: "An unexpected error occurred.",
technicalDetails: error.message,
};
}
const { status, statusText } = error;
// 4xx Client errors
if (status >= 400 && status < 500) {
const messages: Record<number, string> = {
400: "Invalid request. Please check your input.",
401: "Please log in to continue.",
403: "You don't have permission to access this.",
404: "The requested resource was not found.",
422: "The submitted data is invalid.",
429: `Too many requests. Please wait ${RETRY_DELAY_MS / 1000} seconds.`,
};
return {
category: "client-error",
retryable: status === 429, // Only rate limit is retryable
userMessage: messages[status] || `Request failed: ${statusText}`,
technicalDetails: `HTTP ${status}: ${statusText}`,
};
}
// 5xx Server errors
if (status >= 500) {
return {
category: "server-error",
retryable: true,
userMessage: "Server error. Please try again in a moment.",
technicalDetails: `HTTP ${status}: ${statusText}`,
};
}
return {
category: "client-error",
retryable: false,
userMessage: "An unexpected error occurred.",
technicalDetails: `HTTP ${status}: ${statusText}`,
};
}Why good: HTTP-aware classification, appropriate retry logic per status code, clear separation of user vs technical messages
---
Good Example - Combined Classification and Retry
// src/components/error-boundary/smart-recovery-boundary.tsx
import { useState, useCallback, type ReactNode } from "react";
import { ErrorBoundary } from "react-error-boundary";
import type { FallbackProps } from "react-error-boundary";
import { categorizeHttpError, type CategorizedHttpError } from "../utils/error-classification";
const MAX_AUTO_RETRIES = 2;
const RETRY_DELAY_MS = 2000;
interface SmartRecoveryBoundaryProps {
children: ReactNode;
onError?: (categorized: CategorizedHttpError) => void;
}
export function SmartRecoveryBoundary({
children,
onError,
}: SmartRecoveryBoundaryProps) {
const [autoRetryCount, setAutoRetryCount] = useState(0);
const [categorized, setCategorized] = useState<CategorizedHttpError | null>(null);
const handleError = useCallback(
(error: Error) => {
const result = categorizeHttpError(error);
setCategorized(result);
onError?.(result);
},
[onError]
);
const handleReset = useCallback(() => {
setAutoRetryCount(0);
setCategorized(null);
}, []);
return (
<ErrorBoundary
onError={handleError}
onReset={handleReset}
fallbackRender={({ resetErrorBoundary }: FallbackProps) => {
if (!categorized) {
return <div role="alert">An error occurred</div>;
}
const canAutoRetry =
categorized.retryable && autoRetryCount < MAX_AUTO_RETRIES;
// Auto-retry for retryable errors
if (canAutoRetry) {
setTimeout(() => {
setAutoRetryCount((prev) => prev + 1);
resetErrorBoundary();
}, RETRY_DELAY_MS);
return (
<div role="alert">
<p>{categorized.userMessage}</p>
<p>Retrying automatically... (Attempt {autoRetryCount + 1}/{MAX_AUTO_RETRIES})</p>
</div>
);
}
return (
<div role="alert">
<p>{categorized.userMessage}</p>
{process.env.NODE_ENV === "development" && (
<details>
<summary>Technical Details</summary>
<pre>{categorized.technicalDetails}</pre>
</details>
)}
{categorized.retryable && (
<button onClick={resetErrorBoundary}>Try Again</button>
)}
{categorized.category === "client-error" && (
<button onClick={() => window.location.href = "/login"}>
Return to Login
</button>
)}
</div>
);
}}
>
{children}
</ErrorBoundary>
);
}Why good: Combines classification with intelligent retry, auto-retries for transient errors, appropriate actions per error category
---
Error Boundaries - Testing Examples
Extended testing examples for error boundaries. See core.md for core patterns.
Prerequisites: Understand the basic ErrorBoundary component and react-error-boundary library usage from core examples first.
---
Pattern 5: Testing Error Boundary Behavior
Good Example - Comprehensive Boundary Tests
// src/components/error-boundary/error-boundary.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ErrorBoundary } from "./error-boundary";
// Component that throws on demand
function ThrowingComponent({ shouldThrow }: { shouldThrow: boolean }) {
if (shouldThrow) {
throw new Error("Test error");
}
return <div>Content rendered successfully</div>;
}
describe("ErrorBoundary", () => {
// Suppress console.error during tests
const originalError = console.error;
beforeAll(() => {
console.error = vi.fn();
});
afterAll(() => {
console.error = originalError;
});
it("renders children when no error occurs", () => {
render(
<ErrorBoundary>
<ThrowingComponent shouldThrow={false} />
</ErrorBoundary>
);
expect(screen.getByText("Content rendered successfully")).toBeInTheDocument();
});
it("renders fallback UI when error occurs", () => {
render(
<ErrorBoundary fallback={<div>Error fallback</div>}>
<ThrowingComponent shouldThrow={true} />
</ErrorBoundary>
);
expect(screen.getByText("Error fallback")).toBeInTheDocument();
expect(screen.queryByText("Content rendered successfully")).not.toBeInTheDocument();
});
it("calls onError when error is caught", () => {
const handleError = vi.fn();
render(
<ErrorBoundary onError={handleError} fallback={<div>Error</div>}>
<ThrowingComponent shouldThrow={true} />
</ErrorBoundary>
);
expect(handleError).toHaveBeenCalledWith(
expect.any(Error),
expect.objectContaining({
componentStack: expect.any(String),
})
);
});
it("resets error state when reset is called", async () => {
const user = userEvent.setup();
let shouldThrow = true;
const { rerender } = render(
<ErrorBoundary
fallbackRender={({ resetErrorBoundary }) => (
<div>
<span>Error occurred</span>
<button onClick={resetErrorBoundary}>Reset</button>
</div>
)}
>
<ThrowingComponent shouldThrow={shouldThrow} />
</ErrorBoundary>
);
expect(screen.getByText("Error occurred")).toBeInTheDocument();
// Fix the error condition
shouldThrow = false;
// Click reset button
await user.click(screen.getByRole("button", { name: "Reset" }));
// Re-render with fixed component
rerender(
<ErrorBoundary
fallbackRender={({ resetErrorBoundary }) => (
<div>
<span>Error occurred</span>
<button onClick={resetErrorBoundary}>Reset</button>
</div>
)}
>
<ThrowingComponent shouldThrow={shouldThrow} />
</ErrorBoundary>
);
expect(screen.getByText("Content rendered successfully")).toBeInTheDocument();
});
});Why good: Tests all key behaviors - normal render, error fallback, callback, reset, suppresses console.error to keep test output clean
---
Good Example - Testing useErrorBoundary Hook
// src/features/data-loader/data-loader.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ErrorBoundary } from "react-error-boundary";
import { DataLoaderContent } from "./data-loader";
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe("DataLoaderContent with error boundary", () => {
const originalError = console.error;
beforeAll(() => {
console.error = vi.fn();
});
afterAll(() => {
console.error = originalError;
});
beforeEach(() => {
mockFetch.mockReset();
});
it("shows error in boundary when fetch fails", async () => {
const user = userEvent.setup();
mockFetch.mockRejectedValueOnce(new Error("Network error"));
render(
<ErrorBoundary
fallbackRender={({ error }) => (
<div role="alert">Error: {error.message}</div>
)}
>
<DataLoaderContent />
</ErrorBoundary>
);
await user.click(screen.getByRole("button", { name: /load data/i }));
await waitFor(() => {
expect(screen.getByRole("alert")).toHaveTextContent("Network error");
});
});
it("recovers when retry succeeds", async () => {
const user = userEvent.setup();
mockFetch
.mockRejectedValueOnce(new Error("Network error"))
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([{ id: "1", name: "Item 1" }]),
});
render(
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div role="alert">
<span>Error: {error.message}</span>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<DataLoaderContent />
</ErrorBoundary>
);
// Trigger error
await user.click(screen.getByRole("button", { name: /load data/i }));
await waitFor(() => {
expect(screen.getByRole("alert")).toBeInTheDocument();
});
// Reset and retry
await user.click(screen.getByRole("button", { name: /retry/i }));
await user.click(screen.getByRole("button", { name: /load data/i }));
await waitFor(() => {
expect(screen.getByText("Item 1")).toBeInTheDocument();
});
});
});Why good: Tests async error propagation via useErrorBoundary hook, tests recovery flow, proper mock setup and cleanup
---
Good Example - Testing resetKeys Behavior
// src/features/user-profile/user-profile.test.tsx
import { render, screen } from "@testing-library/react";
import { ErrorBoundary } from "react-error-boundary";
function ProfileContent({ userId }: { userId: string }) {
if (userId === "invalid") {
throw new Error("Invalid user");
}
return <div>Profile for {userId}</div>;
}
describe("ErrorBoundary resetKeys", () => {
const originalError = console.error;
beforeAll(() => {
console.error = vi.fn();
});
afterAll(() => {
console.error = originalError;
});
it("auto-resets when resetKeys change", () => {
const { rerender } = render(
<ErrorBoundary
resetKeys={["invalid"]}
fallbackRender={() => <div>Error fallback</div>}
>
<ProfileContent userId="invalid" />
</ErrorBoundary>
);
expect(screen.getByText("Error fallback")).toBeInTheDocument();
// Change to valid user - boundary should reset due to resetKeys change
rerender(
<ErrorBoundary
resetKeys={["valid-user"]}
fallbackRender={() => <div>Error fallback</div>}
>
<ProfileContent userId="valid-user" />
</ErrorBoundary>
);
expect(screen.getByText("Profile for valid-user")).toBeInTheDocument();
});
});Why good: Tests automatic reset behavior when route params change, useful for navigation-triggered resets
---
Pattern 6: Test Utilities for Error Boundaries
Good Example - Reusable Test Helpers
// src/test-utils/error-boundary-helpers.ts
import type { ReactNode } from "react";
import { render, screen } from "@testing-library/react";
import { ErrorBoundary } from "react-error-boundary";
/**
* Render a component wrapped in an error boundary for testing
*/
export function renderWithErrorBoundary(
ui: ReactNode,
options?: {
onError?: (error: Error) => void;
fallbackText?: string;
}
) {
const { onError, fallbackText = "Error occurred" } = options ?? {};
return render(
<ErrorBoundary
onError={onError}
fallbackRender={({ error, resetErrorBoundary }) => (
<div role="alert" data-testid="error-boundary-fallback">
<span>{fallbackText}</span>
<span data-testid="error-message">{error.message}</span>
<button onClick={resetErrorBoundary}>Reset</button>
</div>
)}
>
{ui}
</ErrorBoundary>
);
}
/**
* Assert that an error boundary caught an error
*/
export function expectErrorBoundaryCaught(expectedMessage?: string) {
expect(screen.getByTestId("error-boundary-fallback")).toBeInTheDocument();
if (expectedMessage) {
expect(screen.getByTestId("error-message")).toHaveTextContent(expectedMessage);
}
}
/**
* Assert that content rendered without error
*/
export function expectNoError() {
expect(screen.queryByTestId("error-boundary-fallback")).not.toBeInTheDocument();
}Why good: Reusable helpers reduce test boilerplate, consistent error boundary testing patterns across codebase
---
Good Example - Using Test Helpers
// src/features/chart/chart.test.tsx
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
renderWithErrorBoundary,
expectErrorBoundaryCaught,
expectNoError,
} from "@/test-utils/error-boundary-helpers";
import { Chart } from "./chart";
describe("Chart", () => {
const originalError = console.error;
beforeAll(() => {
console.error = vi.fn();
});
afterAll(() => {
console.error = originalError;
});
it("renders chart with valid data", () => {
renderWithErrorBoundary(<Chart data={[{ x: 1, y: 2 }]} />);
expectNoError();
expect(screen.getByRole("img", { name: /chart/i })).toBeInTheDocument();
});
it("catches error with invalid data", () => {
const onError = vi.fn();
// @ts-expect-error Testing invalid data
renderWithErrorBoundary(<Chart data={null} />, { onError });
expectErrorBoundaryCaught("Cannot read properties of null");
expect(onError).toHaveBeenCalled();
});
it("recovers after reset", async () => {
const user = userEvent.setup();
const { rerender } = renderWithErrorBoundary(<Chart data={[]} />);
// Initially throws with empty data
expectErrorBoundaryCaught();
// Click reset
await user.click(screen.getByRole("button", { name: /reset/i }));
// Re-render with valid data
rerender(
<ErrorBoundary fallbackRender={() => <div>Error</div>}>
<Chart data={[{ x: 1, y: 2 }]} />
</ErrorBoundary>
);
expectNoError();
});
});Why good: Clean, readable tests using helpers, consistent assertion patterns
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/agents-inc/cli/main/src/schemas/metadata.schema.json
category: web-error-handling
slug: error-boundaries
domain: web
author: "@vince"
displayName: Error Boundaries
cliDescription: Error UI handling
usageGuidance: Use when implementing React error boundaries, fallback UIs, or retry logic.
Error Boundaries Reference
Decision frameworks, checklists, and quick-reference tables. See SKILL.md for red flags and core concepts, examples/ for code examples.
---
Decision Framework
When to Use Error Boundary
Is this a React rendering error?
├─ NO → Use try/catch instead
│ ├─ Event handler error → try/catch in handler
│ ├─ Async/Promise error → try/catch or .catch()
│ ├─ setTimeout/callback error → try/catch in callback
│ └─ SSR error → Handle at framework level
└─ YES → Use Error Boundary
├─ Should failure crash entire feature?
│ ├─ YES → Single boundary around feature
│ └─ NO → Multiple granular boundaries
└─ Is recovery possible?
├─ YES → Provide resetErrorBoundary
└─ NO → Show static fallbackChoosing Fallback Pattern
What type of fallback do you need?
├─ Static UI (no props) → Use `fallback` prop
├─ Need error info → Use `fallbackRender` or `FallbackComponent`
├─ Reusable across boundaries → Use `FallbackComponent`
├─ Inline/one-off → Use `fallbackRender`
└─ Context-specific → Create dedicated FallbackComponentClass vs Library
Should you use react-error-boundary library?
├─ Need useErrorBoundary hook → YES, use library
├─ Need resetKeys auto-reset → YES, use library
├─ Minimal dependencies required → NO, write class component
├─ Just need basic boundary → Either works
└─ Production app → YES, use library (more features, maintained)React 19+ createRoot Error Options
Which createRoot error handler to use?
├─ Error caught by ErrorBoundary → onCaughtError (logged + UI handled)
├─ Error NOT caught by any boundary → onUncaughtError (fatal, log immediately)
└─ React auto-recovered (hydration mismatch) → onRecoverableError (warning-level)
Should you use both ErrorBoundary AND createRoot options?
├─ YES → ErrorBoundary for UI, createRoot options for logging
├─ They serve different purposes and complement each other
└─ createRoot options catch errors that escape ALL boundariesBoundary Placement
Where should boundaries be placed?
├─ App root → YES, as last-resort catch-all
├─ Route level → YES, isolate route failures
├─ Feature/widget level → YES, isolate feature failures
├─ Individual component → MAYBE, only for risky components
└─ Every component → NO, too much overheadWhen to Use resetKeys
Should error boundary auto-reset?
├─ User navigates to different route → YES, resetKeys=[pathname]
├─ User views different item → YES, resetKeys=[itemId]
├─ User explicitly retries → NO, use resetErrorBoundary
├─ Timer/automatic retry → YES, resetKeys=[retryCount]
└─ App state changes → MAYBE, depends on error cause---
Red flags and anti-patterns: See SKILL.md <red_flags> section and examples/core.md anti-pattern examples.---
Quick Reference
Error Boundary Checklist
- [ ] Uses class component with
getDerivedStateFromErrorand/orcomponentDidCatch - [ ] No side effects in
getDerivedStateFromError - [ ] Has
onErrorcallback for logging integration - [ ] Provides reset/retry functionality
- [ ] Fallback UI has
role="alert" - [ ] Fallback UI uses button elements (not clickable spans/divs)
- [ ] Error details hidden in production
- [ ] Placed strategically (not just root, not every component)
Fallback UI Checklist
- [ ] Has
role="alert"for accessibility - [ ] Has retry/reset button
- [ ] Provides context about what failed
- [ ] Shows error details in development only
- [ ] Matches visual style of application
- [ ] Uses semantic HTML (buttons, not clickable divs)
Testing Checklist
- [ ] Test renders children when no error
- [ ] Test renders fallback when error occurs
- [ ] Test calls onError callback
- [ ] Test reset functionality works
- [ ] Test resetKeys triggers reset
- [ ] Suppress console.error in tests
What Boundaries Catch
| Scenario | Caught by Boundary? |
|---|---|
| Error in render() | Yes |
| Error in constructor | Yes |
| Error in lifecycle methods | Yes |
| Error in getDerivedStateFromProps | Yes |
| Error in event handler | No - use try/catch |
| Error in setTimeout callback | No - use try/catch |
| Error in async/await | No - use showBoundary |
| Error in Promise | No - use .catch() or showBoundary |
| Error during SSR | No - handle at framework level |
Lifecycle Method Summary
| Method | Phase | Use For | Side Effects |
|---|---|---|---|
getDerivedStateFromError | Render | Update state for fallback UI | NOT allowed |
componentDidCatch | Commit | Logging, error reporting | Allowed |
react-error-boundary Props
| Prop | Type | Purpose |
|---|---|---|
fallback | ReactNode | Static fallback UI |
FallbackComponent | ComponentType<FallbackProps> | Component for 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 |
React 19+ createRoot Error Options
| Option | Type | When Called |
|---|---|---|
onCaughtError | (error, errorInfo) => void | Error caught by an ErrorBoundary |
onUncaughtError | (error, errorInfo) => void | Error NOT caught by any boundary (fatal) |
onRecoverableError | (error, errorInfo) => void | React auto-recovered (hydration, suspense) |
errorInfo parameter contains:
componentStack: string | null- Component tree at error time
captureOwnerStack() (React 19+, dev only):
- Returns owner stack as string or
null - Shows which components CREATED the element (not just tree position)
- Only available in development mode
Related skills
FAQ
Do error boundaries catch async or event handler errors?
No; use try/catch, or the showBoundary() hook to forward async errors into a boundary.
Where should I place error boundaries?
Around feature sections, not just the app root, to create isolated failure domains.