This scenario is a narrow but critical gap in the Next.js & Nuxt Routing Error Pages guide’s coverage of segment-level fallbacks: it handles the one failure a normal error.tsx can never reach.
The exact failure this page solves #
A regular App Router error.tsx is a fallback for the segment it lives in — it renders inside whatever layout wraps it, so it depends on that layout having rendered without throwing. When the exception originates in the root app/layout.tsx itself (a broken provider, a synchronous throw in a shared header, a failed font or context initializer), there’s no parent boundary left standing to catch it. React unwinds the whole tree, and in production the visitor sees a blank white page with nothing in the DOM — no fallback UI, no retry button, nothing.
This is a different failure mode from the segment-level trade-offs covered in Next.js 14 App Router error.tsx vs not-found.tsx Strategies: that page is about choosing the right boundary for expected-vs-missing states within a route tree, while this one is about the single unrecoverable case where the tree’s own root throws before any nested boundary exists to help.
The fix is a special, framework-recognized file: app/global-error.tsx. Next.js treats it as the fallback of last resort. It is conceptually similar to wrapping an entire app in a top-level React error boundary, except Next.js needs a dedicated file convention because the root layout can throw before any client-side boundary component has a chance to mount around it.
Zero-to-working global-error.tsx #
The file below is complete and drop-in ready. It must be a Client Component, and — because it replaces the root layout rather than nesting inside it — it must render its own <html> and <body> tags.
// app/global-error.tsx
'use client'; // global-error.tsx must be a Client Component
import { useEffect } from 'react';
interface GlobalErrorProps {
error: Error & { digest?: string }; // digest correlates with server logs
reset: () => void;
}
export default function GlobalError({ error, reset }: GlobalErrorProps) {
useEffect(() => {
// Runs even though app/layout.tsx never successfully mounted
fetch('/api/telemetry/root-crash', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: error.message,
digest: error.digest,
stack: error.stack,
timestamp: Date.now(),
}),
}).catch(() => {
// Never let a telemetry failure block the fallback UI
});
}, [error]);
return (
// global-error.tsx REPLACES the root layout, so it supplies
// its own <html>/<body> — no shared providers, fonts, or CSS
<html lang="en">
<body style={{ fontFamily: 'system-ui, sans-serif', padding: '2rem' }}>
<h1>Something went critically wrong</h1>
<p>The application failed to render. Our team has been notified.</p>
<button
onClick={() => reset()}
style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
>
Try again
</button>
{error.digest && (
<p style={{ opacity: 0.6, fontSize: '0.8rem' }}>
Reference: {error.digest}
</p>
)}
</body>
</html>
);
}
Copy this file as-is into app/global-error.tsx. Every prop, hook, and markup decision here is deliberate — the step-by-step explanation below walks through why.
Step-by-step explanation #
-
File location and the client directive.
app/global-error.tsxmust live at the top of theappdirectory, sibling tolayout.tsx, and must start with'use client'. Next.js only recognizes this convention as the root-level fallback when the filename and directive are both exact. -
Typed props:
errorandreset. Next.js invokes this component with anerrorobject (extended with an optionaldigeststring that ties the client-visible error to a server-side log entry) and aresetfunction. TypingerrorasError & { digest?: string }keeps the component strict without a cast. -
A full
<html>/<body>document. Because this component activates instead of the root layout — not inside it — it has no wrapping<html>to rely on. Omitting these tags produces a broken or blank document; including them is mandatory, not stylistic. -
Telemetry inside
useEffect. Thefetchcall is deliberately placed in an effect keyed onerror, not in the render body, so it fires exactly once per crash and never blocks the synchronous render of the fallback UI. The.catch(() => {})guard ensures a telemetry outage never throws a second error inside the error boundary itself. -
reset()re-renders the segment, not the page. Clicking the button callsreset(), which tells Next.js to attempt to re-render the crashed root segment. It is not a full page reload or navigation — if the underlying cause (a bad response cached in memory, a stale provider value) hasn’t changed, the same error will recur immediately, which is expected behavior, not a bug in the fallback. -
Production-only activation.
global-error.tsxis intentionally suppressed innext dev, where Next.js prefers to show its own overlay with a full stack trace. It only takes over when the app is served withnext build && next start(or an equivalent production deployment), which is also the only environment where you can validate this file actually works.
Edge cases #
| Scenario | Symptom | Mitigation |
|---|---|---|
Omitting <html>/<body> |
Blank page or a “missing root tags” hydration warning | global-error.tsx replaces the entire root layout, so it must render a complete document, not a fragment |
| Importing the root layout’s CSS or providers | Styles never apply, or the build flags an invalid client/server boundary | Keep the fallback self-contained with inline styles or a small dedicated stylesheet; assume none of app/layout.tsx’s context is reachable |
Expecting reset() to reload the page |
The identical error reappears instantly after clicking “Try again” | reset() only re-renders the segment; pair it with router.refresh() or window.location.reload() if the root cause needs a fresh server request to clear |
Testing exclusively in next dev |
The file appears to do nothing | Next.js suppresses global-error.tsx in development in favor of its overlay; you must run a production build to observe it |
| Assuming identical behavior across Next.js versions | Missing digest, or nesting that doesn’t match older docs |
The file convention and {error, reset} contract have been stable since Next.js 14’s App Router; Next.js 15’s React 19 upgrade only changes how precisely hydration-time errors are attributed, not this API |
Verification steps #
- Temporarily force a crash in the root layout to confirm the fallback actually triggers — for example, add
if (process.env.FORCE_ROOT_CRASH) throw new Error('root crash test');near the top ofapp/layout.tsxand set that env var only for this test run. - Run
next build && next start. Do not test innext dev— the dev overlay will maskglobal-error.tsxentirely and give a false negative. - Load the app in a browser and confirm the fallback UI renders with its own
<html>/<body>, unstyled by the app’s normal CSS, exactly as authored. - Open DevTools → Network and confirm a single POST to
/api/telemetry/root-crashfires once per crash, carryingmessage,digest, andstack. - Click “Try again” and confirm
reset()runs; if the forced-crash condition is still true, the same fallback should reappear immediately — this is the expected segment re-render behavior, not a failure. - Remove the forced-crash line (or its env-gated condition) before shipping; leaving it in place will trip this fallback for every real visitor.
Frequently asked questions #
Why doesn’t my existing error.tsx catch crashes in app/layout.tsx?
A segment-level error.tsx renders inside the root layout’s tree, so it depends on that layout having mounted successfully. When the root layout itself throws, there’s no parent boundary left to fall back to, and only app/global-error.tsx — which sits above the root layout in Next.js’s rendering model — can intercept it.
Does global-error.tsx work when I run next dev?
No. In development, Next.js always shows its built-in error overlay for unhandled exceptions, including ones thrown in the root layout, so you get a full stack trace with source maps. global-error.tsx only activates in a production build served with next start.
Can I share components or CSS between global-error.tsx and my normal layout?
Not reliably. Since global-error.tsx replaces the root layout when it renders, providers, global stylesheets, and fonts declared in app/layout.tsx are not guaranteed to be available to it. Keep the fallback deliberately self-contained — inline styles or a small dedicated stylesheet are the safest choice.
Related #
- Next.js & Nuxt Routing Error Pages — parent guide covering segment-level
error.tsxand route-tree fallback design - Next.js 14 App Router error.tsx vs not-found.tsx Strategies — choosing between the two segment-level fallbacks this page’s root-level fallback sits above
- Nuxt 3 Error Handling with error.vue and clearError — the equivalent root-level fallback convention in Nuxt 3
- Framework-Specific Crash Recovery & Error Handlers — the broader section on framework-native error handling conventions