Error Boundaries — Catch rendering errors in the component tree and display fallback UI
Recipe
import { Component, type ErrorInfo, type ReactNode } from "react";
class ErrorBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError(): { hasError: boolean } {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("ErrorBoundary caught:", error, info.componentStack);
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<RiskyComponent />
</ErrorBoundary>When to reach for this: Wrap any component subtree that might throw during rendering. Essential around data-fetching components, third-party widgets, user-generated content renderers, and route-level layouts.
Working Example
import {
Component,
useState,
useCallback,
type ErrorInfo,
type ReactNode,
} from "react";
// --- Full-featured error boundary with recovery ---
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, info: ErrorInfo) => void;
renderFallback?: (props: {
error: Error;
reset: () => void;
}) => ReactNode;
resetKeys?: unknown[];
}
interface ErrorBoundaryState {
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error, info);
}
componentDidUpdate(prevProps: ErrorBoundaryProps) {
if (this.state.error && prevProps.resetKeys !== this.props.resetKeys) {
// If resetKeys changed, attempt recovery
const changed = this.props.resetKeys?.some(
(key, i) => key !== prevProps.resetKeys?.[i]
);
if (changed) this.reset();
}
}
reset = () => {
this.setState({ error: null });
};
render() {
const { error } = this.state;
const { children, fallback, renderFallback } = this.props;
if (error) {
if (renderFallback) {
return renderFallback({ error, reset: this.reset });
}
return fallback ?? <p>Something went wrong.</p>;
}
return children;
}
}
// --- Usage with recovery UI ---
function UserProfile({ userId }: { userId: string }) {
return (
<ErrorBoundary
resetKeys={[userId]}
onError={(error) => {
// Report to error tracking service
reportToSentry(error);
}}
renderFallback={({ error, reset }) => (
<div className="p-6 bg-red-50 rounded-lg text-center">
<h3 className="text-red-800 font-semibold">Failed to load profile</h3>
<p className="text-red-600 mt-2">{error.message}</p>
<button
onClick={reset}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Try Again
</button>
</div>
)}
>
<ProfileContent userId={userId} />
</ErrorBoundary>
);
}
// --- Granular error boundaries per section ---
function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-4">
<ErrorBoundary fallback={<WidgetError name="Revenue" />}>
<RevenueChart />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError name="Users" />}>
<UserStats />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError name="Activity" />}>
<ActivityFeed />
</ErrorBoundary>
</div>
);
}
function WidgetError({ name }: { name: string }) {
return (
<div className="p-4 border border-red-200 rounded bg-red-50 text-red-700">
{name} widget failed to load.
</div>
);
}What this demonstrates:
- Render-prop fallback with access to the error object and a reset function
resetKeysprop that auto-resets the boundary when dependencies change (e.g., route params)- Error reporting hook (
onError) for Sentry or similar services - Granular boundaries so one broken widget doesn't take down the entire page
Deep Dive
How It Works
- Error boundaries are class components that implement
static getDerivedStateFromError()orcomponentDidCatch(). getDerivedStateFromErrorruns during render to set error state and trigger fallback UI.componentDidCatchruns after commit for side effects like error logging.- Error boundaries catch errors thrown during rendering, in lifecycle methods, and in constructors of their child tree.
- They do NOT catch errors in event handlers, async code, server-side rendering, or errors thrown in the boundary itself.
Parameters & Return Values
| Lifecycle Method | When Called | Purpose |
|---|---|---|
getDerivedStateFromError(error) | During render | Return new state to trigger fallback UI |
componentDidCatch(error, info) | After commit | Side effects: logging, error reporting |
info.componentStack | In componentDidCatch | String showing the component tree path |
Variations
Hook-based error boundary wrapper — use a package or thin wrapper for hook ergonomics:
// With react-error-boundary (popular library)
import { ErrorBoundary } from "react-error-boundary";
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => queryClient.invalidateQueries()}
resetKeys={[routeKey]}
>
<AppContent />
</ErrorBoundary>
);
}
function ErrorFallback({
error,
resetErrorBoundary,
}: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div role="alert">
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
}Nested boundaries with different granularity:
// Page-level boundary catches everything
<ErrorBoundary fallback={<FullPageError />}>
<Layout>
{/* Section-level boundaries isolate failures */}
<ErrorBoundary fallback={<SectionError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError />}>
<MainContent />
</ErrorBoundary>
</Layout>
</ErrorBoundary>TypeScript Notes
- Type the state as
{ error: Error | null }rather than{ hasError: boolean }to preserve the error object for the fallback. - Use
ErrorInfofrom React for thecomponentDidCatchsecond parameter. - The
componentStackproperty onErrorInfois a string, not a structured object.
Gotchas
-
Event handler errors are not caught — Error boundaries only catch errors during React rendering. An
onClickthat throws will not be caught. Fix: Use try/catch in event handlers and set local error state. -
Async errors are not caught — Promises that reject inside
useEffector event handlers are not caught. Fix: Catch async errors and either set local state or re-throw during render (e.g., store in state and throw in render). -
No hook-based error boundaries — React does not provide a hook equivalent of
getDerivedStateFromError. Fix: Use the class component pattern or thereact-error-boundarylibrary. -
Error boundary itself throwing — If the error boundary's own render method throws, it propagates to the nearest parent boundary. Fix: Keep error boundary render methods simple. Never throw in the fallback UI.
-
Recovery without unmount — Resetting
hasErrorto false without remounting children may leave them in a broken state. Fix: Use akeyprop on the children wrapper to force remount on reset.
Alternatives
| Approach | Trade-off |
|---|---|
| Error boundaries | Catches render errors; requires class component |
react-error-boundary | Hook-friendly API, resetKeys; extra dependency |
| Try/catch in event handlers | Manual; doesn't catch render errors |
| Suspense with error handling | Handles async loading errors via thrown promises |
Global window.onerror | Catches uncaught errors; no React-specific recovery |
Real-World Example
From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).
// Production example: Next.js error.tsx with logging, retry, and full-page reload fallback
// File: app/error.tsx
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log to external error tracking service
console.error('Application error:', error);
// reportToSentry(error);
}, [error]);
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center space-y-4">
<h2 className="text-2xl font-bold">Something went wrong</h2>
{error.digest && (
<p className="text-sm text-gray-500">Error ID: {error.digest}</p>
)}
<div className="flex gap-3 justify-center">
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Try again
</button>
<button
onClick={() => window.location.href = '/'}
className="px-4 py-2 border rounded hover:bg-gray-50"
>
Go home
</button>
</div>
</div>
</div>
);
}What this demonstrates in production:
error.tsxis a Next.js App Router convention that automatically wraps the page in an error boundary. You do not need to write a class-based error boundary yourself.- The
useEffectlogs the error on mount. This is the correct place to send errors to Sentry or a similar service. Do not log in the render body because it runs during React's render phase. reset()re-renders the route segment, which retries the failed component. This works well for transient errors like network failures or race conditions.error.digestis a hashed error identifier that Next.js generates for server-side errors. It is safe to display to users (it does not expose stack traces or sensitive details). In development, the full error is shown instead.- The "Go home" button uses
window.location.hrefinstead ofrouter.push()as a deliberate choice. If the error corrupted the React tree or client-side router state,router.push()might fail or render the same broken page. A full page reload viawindow.location.hrefguarantees a clean start. - This
error.tsxmust be a Client Component ('use client'). Error boundaries require client-side React to catch and recover from errors.
FAQs
What is an error boundary and what errors does it catch?
- An error boundary is a class component that catches errors during rendering, lifecycle methods, and constructors of its child tree.
- It displays a fallback UI instead of crashing the entire app.
- It does NOT catch errors in event handlers, async code, SSR, or errors in the boundary itself.
Why are error boundaries class components and not hooks?
- React does not provide a hook equivalent of
getDerivedStateFromErrororcomponentDidCatch. - These lifecycle methods are only available in class components.
- Use the
react-error-boundarylibrary if you prefer a hook-friendly wrapper API.
What is the difference between getDerivedStateFromError and componentDidCatch?
getDerivedStateFromErrorruns during the render phase to update state and trigger the fallback UI. It must be a pure function.componentDidCatchruns after the commit phase for side effects like logging errors to Sentry.- Use both: one for showing the fallback, one for reporting.
How do resetKeys work for automatic error recovery?
<ErrorBoundary
resetKeys={[userId]}
renderFallback={({ error, reset }) => (
<div>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}
>
<ProfileContent userId={userId} />
</ErrorBoundary>- When
resetKeyschange (e.g.,userIdchanges), the boundary automatically resets and retries rendering. - This pairs well with route parameter changes to recover from stale errors.
Gotcha: Why don't error boundaries catch errors in event handlers?
- Error boundaries only catch errors thrown during React's rendering lifecycle.
- An
onClickhandler that throws does not interrupt rendering; it throws in the browser's event loop. - Fix: use try/catch inside event handlers and set local error state.
Gotcha: What happens if the error boundary's own render method throws?
- The error propagates to the nearest parent error boundary.
- If no parent boundary exists, the entire app crashes.
- Fix: keep error boundary render methods and fallback UI simple. Never throw in the fallback.
How do you type an error boundary component in TypeScript?
interface ErrorBoundaryState {
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { error };
}
}- Type state as
{ error: Error | null }to preserve the error object for the fallback. - Use
ErrorInfofrom React forcomponentDidCatch's second parameter.
What is error.digest in Next.js error boundaries?
error.digestis a hashed error identifier generated by Next.js for server-side errors.- It is safe to display to users because it does not expose stack traces or sensitive details.
- In development, the full error is shown instead.
Why does the Next.js error.tsx "Go home" button use window.location.href instead of router.push()?
- If the error corrupted the React tree or client-side router state,
router.push()might fail or render the same broken page. window.location.hreftriggers a full page reload, guaranteeing a clean start.- This is a deliberate production safety pattern.
How should you structure error boundaries for granular error isolation?
- Place a page-level boundary to catch any uncaught errors as a last resort.
- Place section-level boundaries around individual widgets or features so one failure does not take down the entire page.
- Each boundary can show its own localized fallback UI.
How do you reset an error boundary without leaving children in a broken state?
- Simply resetting
hasErrortofalsere-renders the same children, which may still be broken. - Fix: use a
keyprop on the children wrapper to force a full unmount and remount on reset. - This ensures children start fresh with clean state.
Related
- Suspense — Loading state boundaries that pair with error boundaries
- State Machines — Model error states explicitly in state transitions
- Composition — Error boundaries use the composition pattern with
children