A rendering fault in a deeply nested React subtree — uncaught by boundaries placed too broadly — can unmount the entire application shell, leaving thousands of concurrent users on a blank screen while engineers diagnose a stack trace stripped of meaningful context. This page documents deterministic crash-recovery engineering across React, Vue, Svelte, Next.js, Nuxt, and micro-frontend architectures: boundary scoping, reactive state preservation, routing fallbacks, async rollback, shadow-DOM isolation, and non-blocking telemetry.
These patterns extend and complement the boundary scoping model defined in Error Boundary Architecture Fundamentals.
Architecture Overview #
The diagram below maps the four recovery sub-systems covered in this page. Each sub-system is a contained failure domain with its own interception point, fallback surface, and telemetry path.
Error Boundary Scope and React Integration #
Proper error boundary scope prevents cascading UI collapse during rendering anomalies. Boundaries must be explicitly defined across rendering lifecycles, DOM reconciliation phases, and framework initialization sequences — wrapping the entire app tree in a single boundary is the most common scoping mistake; it fragments UX recovery and produces useless fallback states when a feature-level module is the real culprit.
The React Error Boundary Implementation cluster covers production boundary composition in depth, including typed fallback slots, severity tiering, and multi-zone recovery. The TypeScript base class below establishes the core lifecycle contract:
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryProps {
fallback?: ReactNode;
onError?: (error: Error, info: ErrorInfo) => void;
children: ReactNode;
}
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 };
}
// Called during the render phase — must be pure, no side-effects
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return { hasError: true, error };
}
// Called after commit — safe to trigger telemetry dispatch here
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.props.onError?.(error, errorInfo);
}
render(): ReactNode {
if (this.state.hasError) {
return (
this.props.fallback ?? (
<div role="alert" className="ui-fallback" aria-live="polite">
<h3>Component recovery active</h3>
<p>A rendering fault was isolated. Refresh or navigate away to continue.</p>
</div>
)
);
}
return this.props.children;
}
}
Common scoping pitfalls
- Hydration mismatches during SSR client takeover. Mismatched DOM trees bypass synchronous boundaries. Use
suppressHydrationWarningselectively or implement client-side reconciliation guards. - Third-party script injection failures. External scripts executing outside the framework lifecycle require
window.onerrororwindow.addEventListener('error', ...)fallbacks — boundaries do not reach them. - Nested boundary collision and error swallowing. Deeply nested boundaries can mask root causes. Implement strict error propagation policies and log boundary depth.
- Async promise rejections are invisible to class boundaries. Synchronous boundaries cannot catch
Promiserejections — route async failures through dedicated interceptors.
You can find guidance on propagation policies and preventing swallowing in Error Propagation Strategies.
Vue and Svelte Global Error Handlers #
Vue’s app.config.errorHandler and Svelte’s onError lifecycle hook operate at the framework runtime level rather than in the component tree — giving them access to reactive proxies and compiled component instances that class-based boundaries cannot reach. Vue and Svelte Global Error Handlers documents the full integration model; the snippet below shows how to intercept Vue 3 errors and perform deterministic state snapshotting before reactive proxies are invalidated:
import { createApp, App } from 'vue';
import type { ComponentPublicInstance } from 'vue';
type AppStateSnapshot = Record<string, unknown>;
// Serialises store state to IndexedDB with sessionStorage as hot fallback
async function snapshotToIndexedDB(state: AppStateSnapshot): Promise<void> {
const serialized = JSON.stringify(state);
sessionStorage.setItem('crash_snapshot', serialized);
if (!window.indexedDB) return;
await new Promise<void>((resolve) => {
const req = indexedDB.open('app_state_db', 1);
req.onupgradeneeded = (e) =>
(e.target as IDBOpenDBRequest).result.createObjectStore('snapshots');
req.onsuccess = (e) => {
const db = (e.target as IDBOpenDBRequest).result;
const tx = db.transaction('snapshots', 'readwrite');
tx.objectStore('snapshots').put(serialized, 'latest');
tx.oncomplete = () => resolve();
tx.onerror = () => resolve(); // sessionStorage already written
};
});
}
export function attachVueErrorHandler(
app: App,
getStoreState: () => AppStateSnapshot
): void {
app.config.errorHandler = async (
err: unknown,
instance: ComponentPublicInstance | null,
info: string
) => {
const error = err instanceof Error ? err : new Error(String(err));
// Snapshot before Vue tears down reactive proxy references
await snapshotToIndexedDB(getStoreState());
console.error(`[Vue error handler] ${info}`, error, instance);
// Dispatch to telemetry pipeline — see Monitoring section
};
}
State preservation pitfalls
- Concurrent mutations during crash propagation. Race conditions can corrupt snapshots. Implement write locks or atomic transitions before serialization.
- Memory leaks from unmounted error components. Detached listeners prevent GC. Explicitly clear subscriptions in
onUnmountedorcomponentWillUnmount. - Stale closure references in recovered handlers. Captured variables may reference invalidated contexts. Rebind handlers post-recovery.
- Serializing binary payloads. Filter out
Blob,File, orArrayBufferinstances before JSON serialization. - Failing to clear pending watchers. Reactive frameworks retain computed properties post-crash. Explicitly dispose effects during context reset.
For deeper state serialization patterns including draft recovery, see Draft Auto-Save Recovery Workflows and LocalStorage and IndexedDB Sync Strategies.
Next.js and Nuxt Routing Error Recovery #
Crash scenarios frequently originate from data-fetching pipelines, route-resolution failures, and server-side rendering contexts where the component that errors is not the component whose fallback renders. Configuring Next.js and Nuxt Routing Error Pages ensures deterministic fallback rendering when route-level loaders fail or SSR contexts are corrupted during initial page loads.
The RouteRecoveryManager below corrects client-side history state and emits a typed recovery event that downstream instrumentation can subscribe to — avoiding the full-page hard reload that damages session continuity:
interface HydrationErrorPayload {
timestamp: number;
path: string;
stack: string | undefined;
isHydrationError: true;
userAgent: string;
}
export class RouteRecoveryManager {
static handleHydrationFailure(error: Error, currentPath: string): void {
const errorPayload: HydrationErrorPayload = {
timestamp: Date.now(),
path: currentPath,
stack: error.stack,
isHydrationError: true,
userAgent: navigator.userAgent,
};
// Embed error context for DevTools inspection — not user-visible
const script = document.createElement('script');
script.type = 'application/json';
script.id = '__hydration_error__';
script.textContent = JSON.stringify(errorPayload);
document.head.appendChild(script);
// Soft router reset: correct history without triggering a full reload
window.history.replaceState(
{ recovered: true, errorId: crypto.randomUUID() },
'',
currentPath
);
// Notify downstream subscribers (analytics, support chat, retry logic)
window.dispatchEvent(
new CustomEvent('route:recovery', { detail: errorPayload })
);
}
}
Routing layer pitfalls
- Circular redirect loops. Fallback routes must not trigger the same failing data loader. Implement static guard clauses or pre-rendered fallback templates.
- Stale CDN cache serving corrupted payloads. Append cache-busting query parameters or use
Cache-Control: no-storefor error routes. - Mismatched route params during client hydration. Validate param schemas before hydration; reject malformed routes with a generic fallback renderer.
- Network timeout blind spots. Data loaders must enforce explicit
AbortSignaltimeouts — unresolved loaders silently hang route transitions.
Handling hydration mismatches that persist into component rendering is covered in Hydration Mismatch State Recovery.
Async Operation Rollback Triggers #
Intercepting promise rejections, fetch failures, Web Worker crashes, and background-task timeouts requires explicit rollback triggers that revert UI state, cancel pending network requests, and surface actionable failure states — not generic error strings — without triggering full page reloads.
Custom Hooks for Async Error Catching standardizes this interception across data-fetching layers. The executeWithRollback wrapper below enforces capped exponential backoff and respects AbortSignal cancellation so in-flight retries are cleanly cancelled when the user navigates away:
export interface AsyncOperationConfig {
maxRetries: number;
baseBackoffMs: number;
signal?: AbortSignal;
}
export type FailureKind = 'offline' | 'timeout' | 'server_error' | 'aborted' | 'unknown';
export class AsyncRollbackError extends Error {
constructor(
message: string,
public readonly kind: FailureKind,
public readonly cause?: unknown
) {
super(message);
this.name = 'AsyncRollbackError';
}
}
export async function executeWithRollback<T>(
operation: () => Promise<T>,
config: AsyncOperationConfig = { maxRetries: 3, baseBackoffMs: 1000 }
): Promise<T> {
let attempt = 0;
while (attempt < config.maxRetries) {
if (config.signal?.aborted) {
throw new AsyncRollbackError('Aborted by caller', 'aborted');
}
try {
return await operation();
} catch (err) {
if (config.signal?.aborted) {
throw new AsyncRollbackError('Aborted during retry', 'aborted', err);
}
attempt++;
if (attempt >= config.maxRetries) {
const kind: FailureKind =
!navigator.onLine ? 'offline'
: err instanceof DOMException && err.name === 'TimeoutError' ? 'timeout'
: 'server_error';
throw new AsyncRollbackError('Max retries exceeded', kind, err);
}
// Capped exponential backoff with jitter to reduce thundering-herd effect
const delay = config.baseBackoffMs * Math.pow(2, attempt - 1);
await new Promise<void>((resolve) => setTimeout(resolve, delay));
}
}
throw new AsyncRollbackError('Unreachable', 'unknown');
}
Async rollback pitfalls
- Race conditions between retries and navigation. Cancel pending retries in route unmount callbacks — the
AbortSignalpattern above handles this cleanly. - Partial payload delivery causing schema validation failures. Validate JSON schema strictly before any state mutation; reject partial payloads as a recoverable error rather than applying them.
- Worker thread termination mid-calculation. Call
Worker.terminate()and fall back to main-thread execution with degraded capability rather than blocking indefinitely. - Swallowing network error detail. Surface the typed
FailureKind('offline','timeout','server_error') to the UI — generic “something went wrong” strings destroy debuggability.
Micro-Frontend and Web Component Isolation #
Distributed UI architectures require strict crash containment across shadow DOM boundaries, cross-origin widget integrations, and independent deployment lifecycles. Event delegation limits, iframe sandboxing, and modular recovery protocols prevent third-party failures from propagating to the host application shell.
For fallback UI rendering inside isolated widgets, keep fallback templates lightweight — heavy DOM manipulation inside shadow roots during recovery can themselves trigger layout faults on low-memory devices. The WidgetIsolationController below demonstrates shadow DOM error capture and iframe reset patterns:
export class WidgetIsolationController {
// Capture errors bubbling through shadow boundaries before they reach the host
static attachShadowErrorListener(
host: HTMLElement,
callback: (err: Error) => void
): void {
const shadow = host.shadowRoot;
if (!shadow) return;
shadow.addEventListener(
'error',
(e: ErrorEvent) => {
e.stopImmediatePropagation(); // Prevent host from seeing raw ErrorEvent
callback(e.error ?? new Error(e.message));
},
true // Capture phase — fires before bubble phase
);
}
// Hard-reset an iframe widget and reload from a known-good fallback URL
static setupIframeRecovery(
iframe: HTMLIFrameElement,
trustedOrigin: string
): void {
window.addEventListener('message', (event: MessageEvent) => {
if (event.origin !== trustedOrigin) return; // Origin validation is mandatory
if (event.data?.type === 'WIDGET_CRASH') {
iframe.src = 'about:blank'; // Terminate the crashed browsing context
setTimeout(() => {
iframe.src = event.data.fallbackUrl ?? '/widget-fallback.html';
}, 50);
}
});
}
}
Isolation pitfalls
- Cross-origin script execution blocks. Strict
Content-Security-Policyheaders andsandboxattributes on iframes are mandatory — not optional. - Shared global namespace pollution. Isolate dependencies using module federation or strict IIFE wrappers; a leaked global from one micro-frontend can silently corrupt another.
- Event listener detachment during hot module replacement. Re-attach listeners in
connectedCallbackor framework mount hooks — HMR discards listeners bound at module evaluation time. - Shadow DOM does not isolate JavaScript execution. Shadow DOM isolates styles and DOM; a thrown error inside a shadow tree can still crash the host page’s main thread if not caught at the right boundary.
- Untrusted widget dependencies. Always run third-party code in isolated iframes or Web Workers — never inline.
State Implications and Cross-Pillar Interaction #
Framework-level error handlers interact with session persistence, async error catching, and offline recovery in ways that are easy to get wrong under concurrency.
When a Vue errorHandler or React componentDidCatch fires, a snapshot write to IndexedDB is already racing against the framework tearing down reactive proxy references. The strategy is to write the snapshot synchronously to sessionStorage first (fast, synchronous), then flush to IndexedDB asynchronously — the same dual-write pattern used in LocalStorage and IndexedDB Sync Strategies.
On reconnect after a network drop, Cache Warming and Pre-fetching on Reconnect covers how to warm router-level data-loader caches before rehydrating component trees — preventing the route-recovery hydration errors described in the section above from firing again after a stale cache miss.
Boundary events from micro-frontends must also be correlated with server-side traces. Propagating X-Trace-ID or X-Session-ID headers across all requests — including the telemetry endpoint — ensures that a crash surfaced in a widget’s shadow DOM can be joined with the server log that generated the corrupted payload causing it. Component Isolation Techniques covers the host/guest contract that makes this correlation possible.
Monitoring and Observability #
Frontend error boundaries must synchronize with backend observability platforms through structured logging formats, user session correlation IDs, and automated alert thresholds. Telemetry capture must remain non-blocking and conform to data-privacy constraints — stack traces leak file paths, auth tokens, and email addresses if not sanitized before transmission.
export interface TelemetryEvent {
id: string;
type: 'boundary_catch' | 'async_reject' | 'hydration_fail' | 'widget_crash';
payload: Record<string, unknown>;
sessionId: string;
timestamp: number;
traceId?: string; // Propagated from response headers for server correlation
}
export class TelemetryRouter {
private static worker: Worker | null = null;
static init(workerScriptUrl: string): void {
this.worker = new Worker(workerScriptUrl);
// Worker handles batching, deduplication, and retry-on-reconnect
}
static dispatch(event: TelemetryEvent): void {
const sanitized = this.sanitize(event);
if (this.worker) {
this.worker.postMessage(sanitized);
} else if (navigator.sendBeacon) {
// sendBeacon survives page unload — critical for crash-flush scenarios
navigator.sendBeacon('/api/telemetry', JSON.stringify(sanitized));
} else {
fetch('/api/telemetry', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sanitized),
keepalive: true, // Keeps request alive past page navigation
}).catch(() => {/* Best-effort only */});
}
}
private static sanitize(event: TelemetryEvent): TelemetryEvent {
const payload = { ...event.payload };
if (typeof payload.stack === 'string') {
payload.stack = payload.stack
.replace(/\/node_modules\/.*/g, '[REDACTED_MODULE]')
.replace(/email=[^&\s]*/gi, 'email=[REDACTED]')
.replace(/token=[^&\s]*/gi, 'token=[REDACTED]');
}
return { ...event, payload };
}
}
Telemetry pitfalls
- Rate limiting during mass crash events. Implement client-side deduplication (hash the error message + stack) and batched dispatch with a flush interval.
- PII leakage in stack traces. Sanitize payloads before network transmission — the
redactstep above is the minimum; also strip query-string parameters from URLs in error messages. - Network dropouts preventing telemetry flush. The Worker should queue events to
sessionStorageand retry on theonlineevent. - Logging synchronously on the main thread. Offload to a dedicated Worker or use
requestIdleCallback— logging during a crash event compounds the rendering fault. - Missing correlation between client and server traces. Propagate
X-Trace-IDheaders across all requests and embed the trace ID in every telemetry payload. - Alert fatigue from over-sampling. Implement statistical sampling (e.g. 10% of repeated boundary catches) and route by severity — only page on-call for errors that exceed a rate threshold.
Frequently Asked Questions #
How do I determine the optimal scope for an error boundary? Boundaries should wrap logical UI modules — a checkout flow, a media player, a comment thread — rather than individual components or the entire application tree. This balances isolation granularity with recovery overhead, ensuring failures remain contained without fragmenting the user experience into dozens of disconnected fallback states.
What happens to session state during a crash recovery?
State should be serialized to a persistent layer (IndexedDB or sessionStorage) before triggering rollback. On recovery, the application validates the snapshot against the current schema version, reconciles it with server state, and rehydrates the UI context. Schema mismatches between snapshot and current code are a common post-deploy failure mode — version-stamp every snapshot.
Can traditional error boundaries catch asynchronous promise rejections?
No. React class boundaries and Vue errorHandler only capture synchronous rendering errors and synchronous lifecycle exceptions. Async failures require explicit promise wrappers, useErrorBoundary-style hooks, or a global unhandledrejection listener to intercept and route to recovery handlers.
How do I prevent error handlers from causing performance degradation?
Decouple telemetry logging from the main rendering thread using Web Workers or requestIdleCallback. Implement exponential backoff for retries, cache error states to prevent redundant processing, and keep fallback components statically renderable — no lazy imports, no heavy child trees, no async data fetching inside the fallback.
How do I validate crash-recovery paths in CI without triggering real crashes?
Use fault-injection wrappers that throw controlled errors during test runs, assert that boundary fallback UIs mount with the correct role="alert" and aria-live attributes, and spy on TelemetryRouter.dispatch to verify the payload shape. For integration tests, Playwright’s page.evaluate can fire unhandledrejection events directly to exercise global interceptors without needing a real network failure.
Related #
- Error Boundary Architecture Fundamentals — boundary scoping, isolation techniques, and fallback rendering patterns that underpin all framework-specific implementations
- React Error Boundary Implementation — typed boundary composition, severity tiering, and multi-zone recovery in React
- Vue and Svelte Global Error Handlers —
app.config.errorHandler,onErrorCaptured, and Svelte’s error lifecycle in detail - Custom Hooks for Async Error Catching — standardized async interception across React hooks, Vue composables, and Svelte stores
- Session State Persistence and Hydration Fallbacks — IndexedDB dual-write, draft recovery, and hydration mismatch correction