In modern distributed web applications featuring dozens of independent widgets and asynchronous data sources, the failure of a secondary component (such as a recommendation carousel or stock ticker) must never bring down the entire user interface into a catastrophic "White Screen of Death".
Architectural resilience in React is founded upon two declarative containment mechanisms:
Without granular protective boundaries, applications suffer from three critical architectural vulnerabilities:
Suspense boundaries force child components to resolve data sequentially rather than in parallel, compounding page load latency and causing disruptive UI flickering.React treats rendering as a pure, deterministic projection. If an exception is thrown while rendering a component, React walks up the component hierarchy looking for the nearest error handler. If no Error Boundary is encountered, the entire tree unmounts.
Just as a classic try...catch block intercepts synchronous runtime exceptions in procedural code, an Error Boundary acts as a declarative try...catch block for virtual DOM trees.
Suspense operates analogously: when a component suspends on an asynchronous resource via React 19's use(Promise) hook, it pauses rendering (throwing a promise into the React runtime) and instructs the nearest Suspense boundary to display its loadingFallback.
Component requests data via use(Promise); Suspense boundary displays an accurately dimensioned skeleton.
When an exception occurs, the Error Boundary catches the crash, preventing a white screen.
componentDidCatch forwards stack traces, error boundaries, and user metadata to monitoring infrastructure.
User clicks 'Retry' or URL parameters mutate (resetKeys); error state is cleared and data query re-executes.
Which of the following error categories is NOT automatically caught by a React Error Boundary?
Enterprise dashboards featuring dozens of metrics, financial charts, and tables. If an analytics endpoint returns a 500 error or corrupted JSON, only that single chart displays an error card and retry action, leaving 95% of the dashboard completely functional and interactive.
Deploying Suspense boundaries with precision Skeleton components that strictly mirror the target widget's dimensions. This stabilizes Cumulative Layout Shift (CLS) close to zero, avoiding frustrating page jumps when data finishes loading.
In React Server Component architectures, a server rendering crash inside an Error Boundary does not abort the entire HTTP response: the server streams the surrounding layout, and the troubled subsegment gracefully falls back to client rendering or an error placeholder.
The failure interception lifecycle in React is partitioned between synchronous state derivation and asynchronous reporting:
During component rendering, a TypeError occurs (e.g. reading from undefined) or a use(Promise) rejects.
React invokes static getDerivedStateFromError(error). The component updates state to { hasError: true, error }.
In the commit phase, componentDidCatch(error, errorInfo) runs, forwarding the call stack to Sentry or Datadog.
React mounts the fallbackRender component, exposing a retry callback (resetErrorBoundary).
How does the resetKeys array prevent users from remaining trapped on error screens during client-side navigation?
Here is a strict TypeScript implementation of a class-based Error Boundary that avoids the any escape hatch:
'use client';
import * as React from 'react';
export interface FallbackProps {
error: Error;
resetErrorBoundary: () => void;
}
export interface ErrorBoundaryProps {
/** Fallback render function receiving error object and reset callback */
fallbackRender?: (props: FallbackProps) => React.ReactNode;
/** Static fallback ReactNode when reset logic is unnecessary */
fallback?: React.ReactNode;
/** Telemetry callback invoked on error */
onError?: (error: Error, info: React.ErrorInfo) => void;
/** Array of values that trigger an automatic error reset when changed */
resetKeys?: Array<unknown>;
children: React.ReactNode;
}
interface ErrorBoundaryState {
error: Error | null;
}
const initialState: ErrorBoundaryState = { error: null };
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
public override state: ErrorBoundaryState = initialState;
public static getDerivedStateFromError(error: unknown): ErrorBoundaryState {
const normalizedError = error instanceof Error ? error : new Error(String(error));
return { error: normalizedError };
}
public override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
// Invoke telemetry logging (Sentry / Datadog)
this.props.onError?.(error, errorInfo);
}
public override componentDidUpdate(prevProps: ErrorBoundaryProps): void {
const { error } = this.state;
const { resetKeys } = this.props;
// Automatically reset error when resetKeys change
if (error !== null && resetKeys && prevProps.resetKeys) {
const hasKeysChanged = resetKeys.some(
(key, index) => !Object.is(key, prevProps.resetKeys?.[index])
);
if (hasKeysChanged) {
this.reset();
}
}
}
public reset = (): void => {
this.setState(initialState);
};
public override render(): React.ReactNode {
const { error } = this.state;
const { fallbackRender, fallback, children } = this.props;
if (error !== null) {
if (typeof fallbackRender === 'function') {
return fallbackRender({
error,
resetErrorBoundary: this.reset,
});
}
if (fallback !== undefined) {
return fallback;
}
// Default fallback UI
return (
<div role="alert" className="p-4 rounded-xl bg-red-950/30 border border-red-900/50 text-red-200 text-sm">
<p className="font-semibold mb-1">Component failure occurred</p>
<p className="text-xs text-red-400 font-mono mb-3">{error.message}</p>
<button
type="button"
onClick={this.reset}
className="px-3 py-1.5 bg-red-800/60 hover:bg-red-700/60 rounded text-xs font-medium transition-colors cursor-pointer"
>
Retry
</button>
</div>
);
}
return children;
}
}Combining ErrorBoundary and Suspense into a unified defensive wrapper for asynchronous workflows:
'use client';
import * as React from 'react';
import { ErrorBoundary, FallbackProps } from './ErrorBoundary';
export interface AsyncBoundaryProps {
/** Error fallback function */
errorFallback?: (props: FallbackProps) => React.ReactNode;
/** Pending fallback interface (Skeleton) */
loadingFallback: React.ReactNode;
/** Keys triggering automatic error reset */
resetKeys?: Array<unknown>;
/** Error telemetry handler */
onError?: (error: Error, info: React.ErrorInfo) => void;
children: React.ReactNode;
}
/**
* AsyncBoundary: Unified boundary isolating pending and failed asynchronous states
*/
export function AsyncBoundary({
errorFallback,
loadingFallback,
resetKeys,
onError,
children,
}: AsyncBoundaryProps) {
return (
<ErrorBoundary
fallbackRender={errorFallback}
resetKeys={resetKeys}
onError={onError}
>
<React.Suspense fallback={loadingFallback}>
{children}
</React.Suspense>
</ErrorBoundary>
);
}'use client';
import * as React from 'react';
import { AsyncBoundary } from '@/components/resilience/AsyncBoundary';
// Skeleton strictly matching final widget dimensions to guarantee zero CLS
function MetricsSkeleton() {
return (
<div className="p-5 rounded-2xl bg-bg-surface border border-border animate-pulse space-y-4">
<div className="h-4 w-32 bg-bg-elevated rounded-md" />
<div className="h-8 w-24 bg-bg-elevated rounded-md" />
<div className="h-2 w-full bg-bg-elevated rounded-full" />
</div>
);
}
// Error fallback featuring an in-place retry action
function MetricsErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
return (
<div className="p-5 rounded-2xl bg-bg-surface border border-red-900/40 space-y-3">
<div className="flex items-center gap-2 text-red-400">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span className="text-sm font-semibold">Metrics Failed to Load</span>
</div>
<p className="text-xs text-text-muted">{error.message}</p>
<button
type="button"
onClick={resetErrorBoundary}
className="text-xs px-3 py-1.5 bg-bg-elevated text-text-primary rounded-lg hover:bg-border transition-colors cursor-pointer"
>
Retry Fetch
</button>
</div>
);
}
// Internal widget component simulating data retrieval
function MetricsContent({ serverId }: { serverId: string }) {
if (serverId === 'failed-node') {
throw new Error('Node failed-node is offline (503 Service Unavailable)');
}
return (
<div className="p-5 rounded-2xl bg-bg-surface border border-border space-y-2">
<span className="text-xs font-medium text-text-secondary uppercase tracking-wider">
CPU Load ({serverId})
</span>
<div className="text-3xl font-bold text-text-primary">42.8%</div>
<div className="w-full bg-bg-elevated rounded-full h-2 overflow-hidden">
<div className="bg-accent-violet h-full rounded-full" style={{ width: '42.8%' }} />
</div>
</div>
);
}
export function ServerMetricsWidget({ serverId }: { serverId: string }) {
return (
<AsyncBoundary
loadingFallback={<MetricsSkeleton />}
errorFallback={MetricsErrorFallback}
resetKeys={[serverId]} // Mutating serverId automatically clears error state!
onError={(err) => {
console.error(`[Telemetry]: Metrics widget failure for server ${serverId}:`, err);
}}
>
<MetricsContent serverId={serverId} />
</AsyncBoundary>
);
}Error Boundaries exclusively catch errors thrown during the render phase, in constructors, and in class lifecycles. Errors occurring inside setTimeout, event listeners (onClick), or unhandled promises in useEffect will not be captured.
// ❌ ANTIPATTERN: Event listener error will escape ErrorBoundary
function BadComponent() {
const handleClick = async () => {
// This error will NOT trigger the Error Boundary! It triggers Unhandled Promise Rejection
throw new Error('Crash in onClick');
};
return <button onClick={handleClick}>Crash</button>;
}
// ✅ RECOMMENDED: Bridge handler errors using showBoundary from react-error-boundary
import { useErrorBoundary } from 'react-error-boundary';
function GoodComponent() {
const { showBoundary } = useErrorBoundary();
const handleClick = async () => {
try {
await api.doWork();
} catch (err) {
showBoundary(err); // Re-throws into the nearest Error Boundary on next render
}
};
return <button onClick={handleClick}>Safe Action</button>;
}When a parent component suspends on data inside Suspense and its child subsequently suspends on independent data inside a nested Suspense, data fetching becomes strictly serial, doubling load latency. Parallelize requests with Promise.all or hoist boundary envelopes higher in the tree.
Using a 20x20px spinner for an element that eventually occupies 400x300px induces severe Cumulative Layout Shift (CLS). Suspense fallbacks must mirror the final component's dimensions, margins, and aspect ratios.
If a component crashes due to an error in a data-fetching library (like TanStack Query), simply invoking resetErrorBoundary() will re-render the component, which immediately re-reads the cached error and crashes again. Always invalidate the query cache (queryClient.resetQueries()) inside your reset handler.