|$ curl https://forge-ai.dev/api/markdown?path=docs/react/error-boundaries
$cat docs/react-—-error-boundaries.md
updated Last week·13 min read·published
React — Error Boundaries
Introduction
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of crashing the entire application. They work like a try-catch block for React rendering, lifecycle methods, and constructors.
Error boundaries do not catch errors in event handlers, async code (setTimeout, requestAnimationFrame), server-side rendering, or errors thrown in the boundary itself. For those cases, use regular try-catch blocks.
Class Error Boundary
error_boundary.tsx
TSX
| 1 | import React, { Component, ErrorInfo, ReactNode } from "react"; |
| 2 | |
| 3 | interface ErrorBoundaryProps { |
| 4 | children: ReactNode; |
| 5 | fallback?: ReactNode; |
| 6 | onError?: (error: Error, errorInfo: ErrorInfo) => void; |
| 7 | } |
| 8 | |
| 9 | interface ErrorBoundaryState { |
| 10 | hasError: boolean; |
| 11 | error: Error | null; |
| 12 | } |
| 13 | |
| 14 | class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> { |
| 15 | constructor(props: ErrorBoundaryProps) { |
| 16 | super(props); |
| 17 | this.state = { hasError: false, error: null }; |
| 18 | } |
| 19 | |
| 20 | // Static method — called during render phase |
| 21 | // Returns updated state object, or null to update nothing |
| 22 | static getDerivedStateFromError(error: Error): ErrorBoundaryState { |
| 23 | return { hasError: true, error }; |
| 24 | } |
| 25 | |
| 26 | // Called after the error has been committed to the DOM |
| 27 | // Good for error reporting / logging |
| 28 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { |
| 29 | console.error("Error caught by boundary:", error); |
| 30 | console.error("Component stack:", errorInfo.componentStack); |
| 31 | |
| 32 | // Send to error reporting service |
| 33 | this.props.onError?.(error, errorInfo); |
| 34 | } |
| 35 | |
| 36 | render() { |
| 37 | if (this.state.hasError) { |
| 38 | // Custom fallback UI |
| 39 | if (this.props.fallback) { |
| 40 | return this.props.fallback; |
| 41 | } |
| 42 | |
| 43 | // Default fallback |
| 44 | return ( |
| 45 | <div className="p-4 border border-red-500/30 rounded-lg bg-red-500/10"> |
| 46 | <h2 className="text-sm font-mono text-red-400 mb-2"> |
| 47 | Something went wrong |
| 48 | </h2> |
| 49 | <p className="text-xs font-mono text-[#A0A0A0]"> |
| 50 | {this.state.error?.message} |
| 51 | </p> |
| 52 | <button |
| 53 | onClick={() => this.setState({ hasError: false, error: null })} |
| 54 | className="mt-3 px-3 py-1 text-xs font-mono text-red-400 border border-red-500/30 rounded hover:bg-red-500/10" |
| 55 | > |
| 56 | Try again |
| 57 | </button> |
| 58 | </div> |
| 59 | ); |
| 60 | } |
| 61 | |
| 62 | return this.props.children; |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | // Usage |
| 67 | function App() { |
| 68 | return ( |
| 69 | <ErrorBoundary |
| 70 | fallback={<p className="text-red-400">Failed to load dashboard</p>} |
| 71 | onError={(error, info) => { |
| 72 | logErrorToService(error, info.componentStack); |
| 73 | }} |
| 74 | > |
| 75 | <Dashboard /> |
| 76 | </ErrorBoundary> |
| 77 | ); |
| 78 | } |
Granular Error Boundaries
granular.tsx
TSX
| 1 | // Place error boundaries at different levels for different behaviors |
| 2 | |
| 3 | function App() { |
| 4 | return ( |
| 5 | <ErrorBoundary fallback={<FullPageError />}> |
| 6 | <header>Nav</header> |
| 7 | |
| 8 | <main> |
| 9 | <ErrorBoundary fallback={<SidebarError />}> |
| 10 | <Sidebar /> |
| 11 | </ErrorBoundary> |
| 12 | |
| 13 | <ErrorBoundary fallback={<ContentError />}> |
| 14 | <Content /> |
| 15 | </ErrorBoundary> |
| 16 | </main> |
| 17 | |
| 18 | <footer>Footer</footer> |
| 19 | </ErrorBoundary> |
| 20 | ); |
| 21 | } |
| 22 | |
| 23 | // Each boundary catches errors from its children independently |
| 24 | // Sidebar crashing doesn't affect Content and vice versa |
| 25 | |
| 26 | // Reusable error boundary with reset |
| 27 | function Dashboard() { |
| 28 | const [key, setKey] = useState(0); |
| 29 | |
| 30 | return ( |
| 31 | <ErrorBoundary |
| 32 | key={key} |
| 33 | fallback={ |
| 34 | <div> |
| 35 | <p>Widget crashed</p> |
| 36 | <button onClick={() => setKey((k) => k + 1)}>Retry</button> |
| 37 | </div> |
| 38 | } |
| 39 | > |
| 40 | <Widget /> |
| 41 | </ErrorBoundary> |
| 42 | ); |
| 43 | } |
| 44 | |
| 45 | // Error boundary per route |
| 46 | function AppRouter() { |
| 47 | return ( |
| 48 | <Routes> |
| 49 | <Route |
| 50 | path="/dashboard" |
| 51 | element={ |
| 52 | <ErrorBoundary fallback={<DashboardError />}> |
| 53 | <DashboardPage /> |
| 54 | </ErrorBoundary> |
| 55 | } |
| 56 | /> |
| 57 | <Route |
| 58 | path="/settings" |
| 59 | element={ |
| 60 | <ErrorBoundary fallback={<SettingsError />}> |
| 61 | <SettingsPage /> |
| 62 | </ErrorBoundary> |
| 63 | } |
| 64 | /> |
| 65 | </Routes> |
| 66 | ); |
| 67 | } |
| 68 | |
| 69 | // Separate UI state from error state |
| 70 | function FeatureSection() { |
| 71 | const [error, setError] = useState<Error | null>(null); |
| 72 | |
| 73 | if (error) { |
| 74 | return ( |
| 75 | <div className="p-4 border border-yellow-500/30 rounded"> |
| 76 | <p className="text-yellow-400 text-xs">Feature temporarily unavailable</p> |
| 77 | <button |
| 78 | onClick={() => setError(null)} |
| 79 | className="text-xs text-yellow-500 underline mt-2" |
| 80 | > |
| 81 | Retry |
| 82 | </button> |
| 83 | </div> |
| 84 | ); |
| 85 | } |
| 86 | |
| 87 | return <RiskyFeature onError={setError} />; |
| 88 | } |
ℹ
info
Place error boundaries at the granularity you want. A boundary at the top catches everything but shows a full-page fallback. Boundaries around individual widgets keep the rest of the UI functional when one part crashes.
Error Reporting
error_reporting.tsx
TSX
| 1 | // Error boundaries are the ideal place to integrate error reporting |
| 2 | |
| 3 | // Sentry integration example |
| 4 | import * as Sentry from "@sentry/react"; |
| 5 | |
| 6 | function GlobalErrorBoundary({ children }: { children: React.ReactNode }) { |
| 7 | return ( |
| 8 | <ErrorBoundary |
| 9 | fallback={<FallbackScreen />} |
| 10 | onError={(error, errorInfo) => { |
| 11 | // Report to Sentry with component stack |
| 12 | Sentry.withScope((scope) => { |
| 13 | scope.setExtras({ |
| 14 | componentStack: errorInfo.componentStack, |
| 15 | }); |
| 16 | Sentry.captureException(error); |
| 17 | }); |
| 18 | }} |
| 19 | > |
| 20 | {children} |
| 21 | </ErrorBoundary> |
| 22 | ); |
| 23 | } |
| 24 | |
| 25 | // Custom error logging |
| 26 | interface ErrorReport { |
| 27 | message: string; |
| 28 | stack?: string; |
| 29 | componentStack?: string; |
| 30 | url: string; |
| 31 | timestamp: number; |
| 32 | userId?: string; |
| 33 | } |
| 34 | |
| 35 | function logError(error: Error, errorInfo: ErrorInfo) { |
| 36 | const report: ErrorReport = { |
| 37 | message: error.message, |
| 38 | stack: error.stack, |
| 39 | componentStack: errorInfo.componentStack ?? undefined, |
| 40 | url: window.location.href, |
| 41 | timestamp: Date.now(), |
| 42 | }; |
| 43 | |
| 44 | // Send to your error tracking service |
| 45 | fetch("/api/errors", { |
| 46 | method: "POST", |
| 47 | headers: { "Content-Type": "application/json" }, |
| 48 | body: JSON.stringify(report), |
| 49 | }).catch(console.error); |
| 50 | } |
| 51 | |
| 52 | // Context for error reporting |
| 53 | const ErrorReporterContext = createContext< |
| 54 | ((error: Error, info: ErrorInfo) => void) | null |
| 55 | >(null); |
| 56 | |
| 57 | function useReportError() { |
| 58 | return useContext(ErrorReporterContext); |
| 59 | } |
| 60 | |
| 61 | // Usage in a component |
| 62 | function RiskyComponent() { |
| 63 | const reportError = useReportError(); |
| 64 | |
| 65 | useEffect(() => { |
| 66 | try { |
| 67 | riskyOperation(); |
| 68 | } catch (error) { |
| 69 | reportError?.(error as Error, { componentStack: "" }); |
| 70 | } |
| 71 | }, []); |
| 72 | } |
Hooks Alternative Pattern
hooks_pattern.tsx
TSX
| 1 | // React doesn't have a hooks-based error boundary yet, |
| 2 | // but we can create a wrapper pattern |
| 3 | |
| 4 | import { Component, ErrorInfo, ReactNode } from "react"; |
| 5 | |
| 6 | // Reusable component-based wrapper |
| 7 | interface ErrorFallbackProps { |
| 8 | error: Error; |
| 9 | reset: () => void; |
| 10 | } |
| 11 | |
| 12 | function DefaultErrorFallback({ error, reset }: ErrorFallbackProps) { |
| 13 | return ( |
| 14 | <div className="p-6 border border-red-500/30 rounded-lg bg-red-500/5"> |
| 15 | <h2 className="text-sm font-mono text-red-400 mb-2">Error</h2> |
| 16 | <pre className="text-xs font-mono text-[#A0A0A0] mb-3 overflow-auto"> |
| 17 | {error.message} |
| 18 | </pre> |
| 19 | <button |
| 20 | onClick={reset} |
| 21 | className="px-3 py-1 text-xs font-mono text-red-400 border border-red-500/30 rounded" |
| 22 | > |
| 23 | Try Again |
| 24 | </button> |
| 25 | </div> |
| 26 | ); |
| 27 | } |
| 28 | |
| 29 | // Higher-order component pattern |
| 30 | function withErrorBoundary<P extends object>( |
| 31 | WrappedComponent: React.ComponentType<P>, |
| 32 | FallbackComponent: React.FC<ErrorFallbackProps> = DefaultErrorFallback |
| 33 | ) { |
| 34 | return class extends Component< |
| 35 | P, |
| 36 | { error: Error | null } |
| 37 | > { |
| 38 | state = { error: null as Error | null }; |
| 39 | |
| 40 | static getDerivedStateFromError(error: Error) { |
| 41 | return { error }; |
| 42 | } |
| 43 | |
| 44 | componentDidCatch(error: Error, info: ErrorInfo) { |
| 45 | console.error("withErrorBoundary:", error, info.componentStack); |
| 46 | } |
| 47 | |
| 48 | reset = () => this.setState({ error: null }); |
| 49 | |
| 50 | render() { |
| 51 | if (this.state.error) { |
| 52 | return <FallbackComponent error={this.state.error} reset={this.reset} />; |
| 53 | } |
| 54 | return <WrappedComponent {...this.props} />; |
| 55 | } |
| 56 | }; |
| 57 | } |
| 58 | |
| 59 | // Usage of HOC |
| 60 | const SafeDashboard = withErrorBoundary(Dashboard); |
| 61 | const SafeChart = withErrorBoundary(Chart, CustomChartFallback); |
| 62 | |
| 63 | // Inline error boundary with render props |
| 64 | function Catch({ |
| 65 | fallback, |
| 66 | children, |
| 67 | }: { |
| 68 | fallback: (error: Error, reset: () => void) => ReactNode; |
| 69 | children: ReactNode; |
| 70 | }) { |
| 71 | return ( |
| 72 | <ErrorBoundaryWrapper fallback={fallback}> |
| 73 | {children} |
| 74 | </ErrorBoundaryWrapper> |
| 75 | ); |
| 76 | } |
| 77 | |
| 78 | // Usage |
| 79 | <Catch fallback={(error, reset) => ( |
| 80 | <div> |
| 81 | <p>{error.message}</p> |
| 82 | <button onClick={reset}>Retry</button> |
| 83 | </div> |
| 84 | )}> |
| 85 | <AsyncComponent /> |
| 86 | </Catch> |
✓
best practice
Error boundaries are still class components in React. Use the HOC pattern (withErrorBoundary) to apply them declaratively in function components without writing class syntax.
What Error Boundaries Don't Catch
what_not_catch.tsx
TSX
| 1 | // Error boundaries DO NOT catch errors in: |
| 2 | // 1. Event handlers |
| 3 | // 2. Asynchronous code (setTimeout, requestAnimationFrame, Promises) |
| 4 | // 3. Server-side rendering |
| 5 | // 4. Errors thrown in the boundary itself |
| 6 | |
| 7 | // ❌ Event handler errors — use try-catch |
| 8 | function Button() { |
| 9 | function handleClick() { |
| 10 | try { |
| 11 | riskyOperation(); |
| 12 | } catch (error) { |
| 13 | console.error("Button error:", error); |
| 14 | } |
| 15 | } |
| 16 | |
| 17 | return <button onClick={handleClick}>Click</button>; |
| 18 | } |
| 19 | |
| 20 | // ❌ Promise errors — use error boundaries + Suspense or .catch() |
| 21 | async function fetchData() { |
| 22 | try { |
| 23 | const res = await fetch("/api/data"); |
| 24 | if (!res.ok) throw new Error("Failed to fetch"); |
| 25 | return res.json(); |
| 26 | } catch (error) { |
| 27 | // Handle the error, or throw to trigger Suspense error boundary |
| 28 | throw error; |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | // ❌ useEffect errors — wrap in try-catch |
| 33 | function DataLoader() { |
| 34 | useEffect(() => { |
| 35 | try { |
| 36 | riskySetup(); |
| 37 | } catch (error) { |
| 38 | console.error("Setup error:", error); |
| 39 | } |
| 40 | }, []); |
| 41 | } |
| 42 | |
| 43 | // ✅ Render errors — caught by error boundaries |
| 44 | function RenderError() { |
| 45 | throw new Error("This IS caught by error boundary"); |
| 46 | } |
| 47 | |
| 48 | // ✅ Lifecycle method errors — caught by error boundaries |
| 49 | class LifecycleComponent extends Component { |
| 50 | componentDidMount() { |
| 51 | // This IS caught |
| 52 | riskyInit(); |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | // For async errors, use the pattern: |
| 57 | function AsyncErrorBoundary({ children }: { children: React.ReactNode }) { |
| 58 | const [error, setError] = useState<Error | null>(null); |
| 59 | |
| 60 | if (error) { |
| 61 | return <ErrorFallback error={error} reset={() => setError(null)} />; |
| 62 | } |
| 63 | |
| 64 | return ( |
| 65 | <ErrorBoundary fallback={(e) => { setError(e); return null; }}> |
| 66 | {children} |
| 67 | </ErrorBoundary> |
| 68 | ); |
| 69 | } |
$Blueprint — Engineering Documentation·Section ID: REACT-ERR·Revision: 1.0