Error Boundary & Recovery Patterns
Implements comprehensive error handling patterns including React error boundaries, API error responses, retry logic, and graceful degradation.
Error Boundary & Recovery Patterns
Implement robust error handling strategies across React components and backend services with graceful degradation, automatic recovery, and user-friendly fallback UIs.
When to Use This Template
Choose Error Boundary & Recovery Patterns when:
- React components crash and take down the entire page instead of showing fallbacks
- You need systematic error handling with retry logic and circuit breakers
- API failures should degrade gracefully rather than showing blank screens
- You want consistent error reporting and recovery across your application
Consider alternatives when:
- You need global error monitoring and alerting (use Sentry or Datadog)
- Your errors are purely validation-related (use form validation libraries)
- You need distributed tracing across microservices (use OpenTelemetry)
Quick Start
# .claude/skills/error-boundary-recovery-patterns.yml name: error-boundary-recovery-patterns description: Implement error boundaries and recovery strategies prompt: | Add error handling with recovery patterns to the specified code. Implement: 1. React Error Boundaries with fallback UIs 2. Retry logic with exponential backoff 3. Circuit breaker pattern for failing services 4. Graceful degradation for non-critical features 5. Error reporting with context Prioritize user experience — show helpful messages, not stack traces.
Example invocation:
claude "Add error boundaries to the Dashboard page with retry capability for failed API calls"
Generated Error Boundary:
function DashboardErrorFallback({ error, resetErrorBoundary }) { return ( <div className="p-6 bg-red-50 rounded-lg text-center"> <h3 className="text-lg font-semibold text-red-800"> Dashboard failed to load </h3> <p className="text-red-600 mt-2"> {error.message || "An unexpected error occurred"} </p> <button onClick={resetErrorBoundary} className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700" > Try Again </button> </div> ); }
Core Concepts
Error Handling Strategy Matrix
| Error Type | Strategy | User Impact | Recovery |
|---|---|---|---|
| Render error | Error Boundary + fallback UI | Component-level | Manual retry |
| API timeout | Retry with backoff | Loading state | Automatic |
| Service down | Circuit breaker + cache | Stale data shown | Automatic |
| Auth expired | Token refresh + replay | Transparent | Automatic |
| Network offline | Queue + sync later | Offline banner | On reconnect |
React Error Boundary with Recovery
import { Component, ReactNode } from 'react'; interface Props { children: ReactNode; fallback: (props: { error: Error; retry: () => void }) => ReactNode; onError?: (error: Error, errorInfo: React.ErrorInfo) => void; isolationLevel?: 'page' | 'section' | 'component'; } interface State { error: Error | null; errorCount: number; } class RecoverableErrorBoundary extends Component<Props, State> { state: State = { error: null, errorCount: 0 }; static getDerivedStateFromError(error: Error): Partial<State> { return { error }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { this.setState(prev => ({ errorCount: prev.errorCount + 1 })); this.props.onError?.(error, errorInfo); // Report to error tracking service console.error(`[ErrorBoundary:${this.props.isolationLevel}]`, { error: error.message, componentStack: errorInfo.componentStack, recoveryAttempts: this.state.errorCount }); } handleRetry = () => { this.setState({ error: null }); }; render() { if (this.state.error) { if (this.state.errorCount > 3) { return <PermanentErrorFallback error={this.state.error} />; } return this.props.fallback({ error: this.state.error, retry: this.handleRetry }); } return this.props.children; } }
Retry with Exponential Backoff
async function withRetry<T>( fn: () => Promise<T>, options: { maxRetries?: number; baseDelay?: number; maxDelay?: number; retryableErrors?: (error: Error) => boolean; } = {} ): Promise<T> { const { maxRetries = 3, baseDelay = 1000, maxDelay = 10000, retryableErrors = (err) => { // Retry network errors and 5xx, not 4xx if (err.name === 'TypeError') return true; // network error const status = (err as any).status; return status >= 500 || status === 429; } } = options; let lastError: Error; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error; if (attempt === maxRetries || !retryableErrors(lastError)) { throw lastError; } const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay); const jitter = delay * (0.5 + Math.random() * 0.5); await new Promise(resolve => setTimeout(resolve, jitter)); } } throw lastError!; } // Usage const data = await withRetry(() => fetch('/api/dashboard').then(r => r.json()), { maxRetries: 3, baseDelay: 500 });
Circuit Breaker Pattern
class CircuitBreaker { private failures = 0; private lastFailure = 0; private state: 'closed' | 'open' | 'half-open' = 'closed'; constructor( private threshold: number = 5, private resetTimeout: number = 30000 ) {} async execute<T>(fn: () => Promise<T>, fallback?: () => T): Promise<T> { if (this.state === 'open') { if (Date.now() - this.lastFailure > this.resetTimeout) { this.state = 'half-open'; } else if (fallback) { return fallback(); } else { throw new Error('Circuit breaker is open'); } } try { const result = await fn(); this.onSuccess(); return result; } catch (error) { this.onFailure(); if (fallback) return fallback(); throw error; } } private onSuccess() { this.failures = 0; this.state = 'closed'; } private onFailure() { this.failures++; this.lastFailure = Date.now(); if (this.failures >= this.threshold) { this.state = 'open'; } } }
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
maxRetries | number | 3 | Maximum retry attempts before giving up |
backoffBase | number | 1000 | Base delay in ms for exponential backoff |
circuitThreshold | number | 5 | Failures before circuit breaker opens |
circuitResetMs | number | 30000 | Time before circuit breaker tests again |
isolationLevel | string | "section" | Error boundary scope: page, section, component |
reportErrors | boolean | true | Send errors to tracking service |
Best Practices
-
Nest error boundaries at multiple levels — Use a page-level boundary to catch catastrophic failures and section-level boundaries to isolate independent features. If the comments section crashes, the main content should still render. Never rely on a single top-level boundary.
-
Distinguish retryable from permanent errors — Network timeouts and 503s are retryable. A 404 or 422 validation error will fail every time. Check the error type before retrying to avoid wasting time and API quota on guaranteed failures.
-
Show loading states during retry, not errors — When automatically retrying a failed request, keep showing the loading spinner rather than flashing an error message for 500ms. Only show the error UI after all retry attempts are exhausted.
-
Add jitter to backoff delays — When multiple clients retry simultaneously (thundering herd), identical backoff timing causes them to all retry at the same moment. Adding random jitter (50-100% of the delay) spreads retries across time and reduces server load.
-
Cache the last successful response as fallback — When a circuit breaker trips open, serve the last cached successful response with a "data may be stale" indicator rather than showing a blank error state. Stale data is almost always better than no data for read-only endpoints.
Common Issues
Error boundary not catching async errors — React Error Boundaries only catch errors during rendering, lifecycle methods, and constructors. They do not catch errors in event handlers, async code, or setTimeout callbacks. Wrap async operations in try/catch and use state to trigger boundary re-renders via setState that throws.
Infinite retry loop consuming resources — The retry function keeps attempting requests against a permanently down service, consuming bandwidth and battery. Always set a maximum retry count and implement a circuit breaker that stops retries entirely after repeated failures. Combine with exponential backoff to give the service time to recover.
Component re-mounts losing user input after recovery — When the error boundary re-renders after a retry, the child component tree re-mounts from scratch, losing form input, scroll position, and local state. Lift critical state above the error boundary or persist it to sessionStorage before the error boundary resets, then restore it on re-mount.
Reviews
No reviews yet. Be the first to review this template!
Similar Templates
Full-Stack Code Reviewer
Comprehensive code review skill that checks for security vulnerabilities, performance issues, accessibility, and best practices across frontend and backend code.
Test Suite Generator
Generates comprehensive test suites with unit tests, integration tests, and edge cases. Supports Jest, Vitest, Pytest, and Go testing.
Pro Architecture Workspace
Battle-tested skill for architectural, decision, making, framework. Includes structured workflows, validation checks, and reusable patterns for development.