s/error.tsx
"use client";
import { useEffect, useState } from "react";
interface MetricsErrorProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function MetricsErrorBoundary({ error, reset }: MetricsErrorProps) {
const [isRetrying, setIsRetrying] = useState(false);
useEffect(() => {
// Forward error payload to telemetry infrastructure
if (typeof window !== "undefined" && window.TELEMETRY) {
window.TELEMETRY.captureException(error, {
context: "metrics_segment_render",
digest: error.digest,
});
}
}, [error]);
const handleRecovery = () => {
setIsRetrying(true);
reset();
// Reset loading state after a brief delay to allow React to re-mount
setTimeout(() => setIsRetrying(false), 1500);
};
return (
<section
role="alert"
aria-live="polite"
className="flex flex-col items-center justify-center min-h-[200px] p-6 bg-slate-50 border border-slate-200 rounded-lg"
>
<h3 className="text-lg font-semibold text-slate-800 mb-2">
Metrics data unavailable
</h3>
<p className="text-sm text-slate-500 mb-4 text-center max-w-md">
The visualization pipeline encountered a rendering exception.
Your session remains active; other modules are unaffected.
</p>
<button
onClick={handleRecovery}
disabled={isRetrying}
className="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isRetrying ? "Recovering..." : "Reload Segment"}
</button>
</section>
);
}
### Step 2: Understand the `reset()` Mechanism
The `reset` prop triggers a re-render of the failed route segment. Under the hood, Next.js unmounts the error boundary's fallback UI and re-initializes the component tree within that segment. This is not a full page navigation; it is a targeted React reconciliation pass.
Important architectural note: `reset()` does not guarantee success. If the underlying data source or component logic remains broken, the error will re-throw immediately. Production implementations should pair `reset()` with loading states, exponential backoff, or user confirmation to prevent infinite render loops.
### Step 3: Configure the Fallback Hierarchy
Next.js resolves errors using a nearest-ancestor strategy. When a component throws, the framework traverses up the route tree until it finds an `error.tsx` file. If none exists in the immediate segment, it bubbles to parent segments.
For catastrophic failures that occur in the root layout (e.g., authentication middleware crashes, global context initialization failures), Next.js requires a `global-error.tsx` file in the `app/` directory. This file acts as the final safety net.
```typescript
// app/global-error.tsx
"use client";
import { useEffect } from "react";
interface GlobalErrorProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function GlobalErrorBoundary({ error, reset }: GlobalErrorProps) {
useEffect(() => {
// Critical path logging for root-level failures
console.error("[CRITICAL] Root layout exception:", error);
}, [error]);
return (
<html>
<body>
<div className="flex items-center justify-center min-h-screen bg-white">
<div className="text-center p-8">
<h1 className="text-2xl font-bold text-gray-900 mb-3">
Application Initialization Failed
</h1>
<p className="text-gray-600 mb-6">
A critical error prevented the core shell from loading.
</p>
<button
onClick={() => reset()}
className="px-5 py-2.5 bg-gray-900 text-white rounded-md hover:bg-gray-800"
>
Restart Application
</button>
</div>
</div>
</body>
</html>
);
}
Note the structural difference: global-error.tsx must render its own <html> and <body> tags because it operates outside the root layout. It cannot inherit global styles or providers from layout.tsx. This isolation is intentional; it ensures the fallback UI renders even when the application shell is completely broken.
Architecture Decisions & Rationale
- File-System Mapping: Next.js compiles
error.tsx into a React Error Boundary at build time. This eliminates manual wrapper components and ensures boundaries align with data-fetching boundaries.
- Client Directive Requirement: Error boundaries rely on React's class-based lifecycle or
useEffect hooks for error capture. Server Components cannot maintain this state, making "use client" mandatory.
- Segment Isolation: By placing
error.tsx at the route segment level, you guarantee that failures do not cross module boundaries. A broken analytics chart never affects the navigation shell or user profile module.
- Digest Propagation: The
digest property is a Next.js-generated hash that correlates client-side errors with server-side logs. Always forward this to your observability platform for cross-environment traceability.
Pitfall Guide
1. Omitting the "use client" Directive
Explanation: error.tsx files default to Server Components in the App Router. Without the directive, Next.js throws a compilation error because error boundaries require client-side state management.
Fix: Always include "use client" at the top of the file. Verify with next dev that the boundary compiles without hydration warnings.
2. Infinite Reset Loops
Explanation: Calling reset() without checking component state or data availability can trigger rapid re-renders if the underlying error persists. This floods the browser's event loop and degrades performance.
Fix: Implement a retry counter or debounce mechanism. Disable the recovery button after two failed attempts and prompt the user to refresh the page or contact support.
3. Misplacing the Boundary File
Explanation: Placing error.tsx in a parent directory when the failure occurs in a deeply nested child segment causes unnecessary UI replacement. The nearest boundary catches the error, so incorrect placement reduces isolation granularity.
Fix: Align error.tsx files with data-fetching boundaries. If a specific route segment fetches its own data, place the boundary in that segment. Use parent boundaries only for shared module failures.
4. Ignoring the digest Property
Explanation: The digest string is a Next.js-generated identifier that links client errors to server logs. Omitting it from telemetry payloads makes cross-environment debugging nearly impossible.
Fix: Always extract error.digest and attach it to your error tracking payload. Use it to query server-side stack traces in your observability dashboard.
5. Attempting Data Fetching Inside error.tsx
Explanation: error.tsx is a Client Component. While you can use fetch or SWR inside it, doing so defeats the purpose of error isolation. The boundary should display fallback UI, not attempt to recover data.
Fix: Keep error.tsx strictly presentational. If recovery requires fresh data, trigger a parent-level data refetch via context or URL state, not inside the boundary itself.
6. Over-Reliance on global-error.tsx
Explanation: Using only the global fallback for all errors removes granular isolation. Users experience full application crashes for minor widget failures, increasing support tickets and abandonment.
Fix: Implement segment-level boundaries for every data-heavy route. Reserve global-error.tsx exclusively for root layout, middleware, or authentication failures.
7. Missing Async Error Handling
Explanation: React Error Boundaries only catch synchronous rendering errors. Async operations (e.g., setTimeout, Promise rejections, event handlers) bypass the boundary entirely.
Fix: Wrap async callbacks in try/catch blocks. Use React's onError callback in createRoot or hydrateRoot for global async error capture. Log async failures separately from rendering errors.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Isolated widget failure (e.g., chart, table) | Segment-level error.tsx | Contains failure to non-critical module; preserves shell UI | Low (single file, minimal maintenance) |
| Route-level data fetch failure | Parent segment error.tsx | Catches all child rendering errors; aligns with data boundary | Medium (requires careful tree mapping) |
| Root layout or auth middleware crash | global-error.tsx | Final safety net; operates outside broken shell | Low (one-time setup, high ROI) |
| Async operation failure (e.g., WebSocket, timeout) | Component-level try/catch + state fallback | Error boundaries do not catch async exceptions | Medium (requires explicit handling per module) |
| Multi-tenant dashboard with shared shell | Nested boundaries per tenant module | Prevents cross-tenant UI contamination; isolates data pipelines | High (initial setup complexity, long-term stability) |
Configuration Template
// app/[tenant]/dashboard/error.tsx
"use client";
import { useEffect, useState, useCallback } from "react";
import { logger } from "@/lib/observability";
interface TenantErrorProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function TenantDashboardError({ error, reset }: TenantErrorProps) {
const [retryCount, setRetryCount] = useState(0);
const [isRecovering, setIsRecovering] = useState(false);
useEffect(() => {
logger.error("Tenant dashboard segment failure", {
error: error.message,
digest: error.digest,
stack: error.stack,
tenantContext: "dynamic_route",
});
}, [error]);
const handleRecovery = useCallback(() => {
if (retryCount >= 2) return;
setIsRecovering(true);
setRetryCount((prev) => prev + 1);
reset();
setTimeout(() => setIsRecovering(false), 1200);
}, [reset, retryCount]);
return (
<div className="flex flex-col items-center justify-center p-8 bg-white border border-gray-200 rounded-xl shadow-sm">
<div className="w-12 h-12 mb-4 rounded-full bg-amber-100 flex items-center justify-center">
<svg className="w-6 h-6 text-amber-600" 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>
</div>
<h2 className="text-lg font-semibold text-gray-900 mb-2">Dashboard data unavailable</h2>
<p className="text-sm text-gray-500 mb-6 text-center max-w-sm">
A rendering exception occurred in this module. Your session and other features remain active.
</p>
{retryCount < 2 ? (
<button
onClick={handleRecovery}
disabled={isRecovering}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-all"
>
{isRecovering ? "Recovering..." : "Attempt Recovery"}
</button>
) : (
<p className="text-xs text-gray-400">
Maximum recovery attempts reached. Please refresh the page.
</p>
)}
</div>
);
}
Quick Start Guide
- Create the boundary file: Add
error.tsx to the route segment that requires fault isolation. Ensure it contains "use client" at the top.
- Define the component signature: Accept
error and reset props. Type error as Error & { digest?: string } for Next.js compatibility.
- Implement telemetry: Use
useEffect to forward error.message, error.stack, and error.digest to your observability platform.
- Build the recovery UI: Render a fallback interface with a recovery button. Wrap
reset() in state management to prevent rapid re-renders.
- Validate isolation: Intentionally throw an error in a child component. Verify that only the target segment displays the fallback UI while the rest of the application remains functional.