Catching the root layout
The App Router's global-error.tsx convention, the outermost Error Boundary that catches a throw in the root layout itself, the one failure no other error.tsx can reach.
You have app/error.tsx wired. After the last lesson it feels like the whole app is covered: any segment can throw, and the user lands on a friendly failure screen instead of a crash. Then one day a deploy goes out with a bad environment variable, and the first thing app/layout.tsx does at request time is read it. The root layout throws. The cause could be a misconfigured provider, a missing env var, or an auth call that fails before a single page renders, but the cause doesn’t change what happens next: the user does not get your error.tsx. They get the browser’s stark default error page, with no branding, no “Try again”, and no message you wrote. One bad deploy takes the entire site down to a blank screen.
This is the gap the last lesson left open on purpose, and closing it is the work of this lesson. You will name the one file that catches a throw in the root layout, understand why it is shaped the way it is, and learn what must never go inside it. By the end you will ship app/global-error.tsx as your site’s last line of defense. The lesson is short and rests almost entirely on what you already know: one new file, two hard rules, and one discipline.
Why your error.tsx lets the root layout through
Section titled “Why your error.tsx lets the root layout through”The last lesson taught the rule that explains this. An Error Boundary catches throws from its descendants, but the layout it sits beside is its parent, so a render error in that layout happens before the boundary exists to catch it.
Apply that rule one level up. The framework places app/error.tsx inside app/layout.tsx, in the layout’s subtree rather than around it. For the reason above, app/error.tsx cannot catch a throw in app/layout.tsx. And the root layout is the top of the application tree, with no error.tsx above it. A throw there has nowhere to bubble up to, so it escapes every boundary you have written and lands on the framework’s bare fallback.
That is the precise shape of the gap. The segment files from the last lesson cover everything under the root layout, but nothing covers the root layout itself. Scrub through the failing render below and watch the throw walk straight out of the tree:
app/layout.tsx
root layout
The error.tsx boundary lives inside the root layout — it is the layout's child.
error.tsx boundary app/error.tsx, exactly as the last lesson taught.
app/layout.tsx
root layout Throws here — outside the error.tsx ring. The boundary is its child, so it does not exist yet.
error.tsx boundary
app/layout.tsx
root layout Throws here — outside the error.tsx ring. The boundary is its child, so it does not exist yet.
error.tsx boundary error.tsx above the root layout — the throw escapes the whole app and the user gets the framework's bare fallback.
The boundary is the layout’s child, so a throw in the layout starts outside the boundary’s reach, and with nothing above the root to catch it, the throw runs clear off the top of the tree. That is why you need a boundary above the root layout.
global-error.tsx: the boundary above the root layout
Section titled “global-error.tsx: the boundary above the root layout”The file that fills the gap is global-error.tsx, at app/global-error.tsx. It is the one Error Boundary the framework wires above the root layout, the outermost boundary in the entire App Router tree. Errors that escape every error.tsx, errors in the root layout itself, even errors in the framework’s own root render all surface here.
The mental model fits in one line: every other error.tsx is a boundary inside the app shell, while global-error.tsx is the boundary around the shell, the last net before the browser default. In the diagram you just scrubbed, the throw escaped the top of the tree with nothing to catch it. global-error.tsx is the box that now sits there to catch it, and it wraps app/layout.tsx itself.
Here is the whole file. It is short on purpose:
'use client';
export default function GlobalError({ error, unstable_retry,}: { error: Error & { digest?: string }; unstable_retry: () => void;}) { return ( <html lang="en"> <body> <h2>Something went wrong</h2> {error.digest != null && <p>Reference: {error.digest}</p>} <button onClick={() => unstable_retry()}>Try again</button> </body> </html> );}Almost all of this you have seen. The 'use client' directive, the error and unstable_retry props, the error.digest shown so a user can paste a reference into a support ticket, and the “Try again” button wired to unstable_retry() are the exact contract from error.tsx. The props are identical, so there is nothing new to learn about how the file receives its data. (As before, the default export is the sanctioned exception to the named-exports rule, since the framework finds these convention files by their default export.)
The one new thing is highlighted: this file returns <html> and <body>. No other component you have written does that. Why it must do this, and why it must also start with 'use client', are the two non-negotiables. The next two sections explain each.
'use client', the same reason as error.tsx
Section titled “'use client', the same reason as error.tsx”global-error.tsx, like error.tsx, must start with 'use client'. The reason is the one you already have: React Error Boundaries are stateful class components, built on getDerivedStateFromError and componentDidCatch, and both the class machinery and its state run on the client only. Omit the directive and the build fails. There is nothing new to work out here; it is the same rule, one level up.
This has one consequence worth stating. Because the file must be a Client Component, the metadata and generateMetadata exports are not supported in global-error.tsx. If your catastrophe screen needs a tab title, render a <title> element inside the returned JSX instead.
Why it must render its own <html> and <body>
Section titled “Why it must render its own <html> and <body>”This one is worth slowing down for, because once you understand the reason the rule is easy to remember.
Normally, app/layout.tsx renders the <html> and <body> that wrap your whole app, and every page renders inside that shell. That is the arrangement you have relied on since you first wrote a layout. But global-error.tsx fires precisely because the root layout crashed. By the time it renders, the root layout is gone, and so are the <html> and <body> it would have produced. There is no parent document for global-error.tsx to render into, because the shell it would normally slot inside is the exact thing that just failed.
So global-error.tsx does not render inside the root layout. It replaces it. When it is active, it is rendered as the entire document, which means it has to reconstruct the document skeleton itself. That is what the <html> and <body> in the file are for. The official framework docs put it in one line: this file replaces the root layout when active.
Compare the two arrangements side by side. On the left is the normal case, where the layout owns the document and the page lives inside it. On the right is the global-error case, where the layout is gone and global-error.tsx is the document:
export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> );}app/layout.tsx renders the document shell, and your page renders inside it.
export default function GlobalError(/* props */) { return ( <html lang="en"> <body>{/* the catastrophe screen */}</body> </html> );}The root layout crashed, so it is gone. global-error.tsx is rendered as the whole document and must produce the shell itself.
The failure mode follows directly. Omit the tags and you ship a document with no <html> and no <body>, which renders as a blank page in production. That is the worst possible outcome for the one file whose entire job is the catastrophe screen. So the tags here are not boilerplate; they are the reason the page renders at all. The chain is short enough to carry in your head: the root layout is what crashed, so no shell is left, so global-error.tsx is the shell now.
Verify it in a production build: the dev trap
Section titled “Verify it in a production build: the dev trap”You met this trap last lesson with error.tsx, and it matters even more here, because the whole site is at stake rather than one segment.
In next dev, when the root layout throws, the Next.js error overlay covers the screen with the full message, the full stack, and the developer’s diagnostic. Current versions of Next.js do render global-error.tsx underneath the overlay in development, a deliberate change so you can preview it. But the overlay sits in front, so in dev you are still looking at the developer experience, not the user’s.
The rule is identical to the one you already have: never sign off on global-error.tsx from next dev alone. Build the app and run the production build locally to see the real thing, with no overlay, the generic message, the digest, and your actual layout. The overlay covering the file in development is exactly why people ship a broken global-error.tsx and never notice until a real user hits it.
A page that must not fail
Section titled “A page that must not fail”That leaves the discipline behind the file, which is the part that generalizes well beyond global-error.tsx.
global-error.tsx is the last line of defense. There is no boundary below it. If it throws while rendering, the user gets nothing recoverable, because the catastrophe screen has itself become the catastrophe. So this must be the most defensive file in your codebase. Its one job is to render a page that cannot itself fail.
What belongs in it is the catastrophe UI and nothing more: a calm message, the error.digest for support correlation, a “Try again” wired to unstable_retry(), a stripped-down brand-aligned shell, and a path to contact support. It is also the right home for one piece of logic: the report to your error-tracking service. When global-error.tsx fires, your most serious failure has just happened, so this is exactly where you notify monitoring, using the same useEffect-reports-the-error pattern you saw in error.tsx. The full integration comes much later in the course, so for now it is enough to know the report lives here. Fleshed out, the file looks like this:
'use client';
import { useEffect } from 'react';
export default function GlobalError({ error, unstable_retry,}: { error: Error & { digest?: string }; unstable_retry: () => void;}) { useEffect(() => { reportToMonitoring(error); }, [error]);
return ( <html lang="en"> <body> <h2>Something went wrong</h2> {error.digest != null && <p>Reference: {error.digest}</p>} <button onClick={() => unstable_retry()}>Try again</button> <a href="mailto:support@example.com">Contact support</a> </body> </html> );}What must never go in is business logic, data fetching, or anything that can throw. The reason is worth saying plainly: an error page that itself errors is unrecoverable. The fetch you add to render a “richer” error screen is the fetch that, on a bad day, fails and leaves the user staring at the browser default after all.
Two concrete constraints follow from that rule. They look like separate rules at first, though by the end of this section you will see they are the same one.
The first is styling. When global-error.tsx renders, the global CSS, the fonts, and the design-system providers imported by the crashed root layout may not be in effect, because the layout that loaded them is the thing that is gone. So keep the page simple: minimal inline styles or a small set of Tailwind utility classes (the framework’s Tailwind layer is still available), no design-system providers, no dependency on a custom font, and no client-only context providers. You cannot assume that anything the dead layout used to set up is still there.
The second is internationalization. Locale-aware copy needs the i18n setup to have loaded before the crash, and for a failure at the top of the layout, it usually has not. So keep global-error.tsx to a single default-locale string (“Something went wrong” and the digest), not a translated message. Doing i18n properly is a later chapter; here, the only rule is to not depend on it.
Both constraints come from one fact. The styling you reach for and the locale you reach for are both set up above you in the tree, and the things above you in the tree are exactly the things that just failed. So depend on none of them: assume nothing above you survived. That principle is the real takeaway of the lesson, and it generalizes far past this one file.
The complete error surface
Section titled “The complete error surface”Step back and put the pieces together, because you have now seen all of them.
app/error.tsx catches errors in app/page.tsx and in any nested segment that has no error.tsx of its own. app/global-error.tsx catches errors in app/layout.tsx plus anything that escapes app/error.tsx. They cover different scopes and do different jobs, so they are complementary rather than redundant, and the experienced default ships both at the app root.
It helps to set this against the last lesson’s frame. There you learned the four states of a segment, populated, in-flight, missing, and failed, along with the four files that handle them. This lesson adds the one boundary that sits above the whole app, for the case where the shell itself fails. Put together, those are the complete error surface of an App Router application. Nothing on a route, and nothing around the route, is left without a screen you wrote.
So this is the configuration to leave with: both files at the root, each with one clear job.
Directoryapp
- layout.tsx the app shell
- error.tsx catches everything under the shell
- global-error.tsx catches the shell itself
- page.tsx
On top of that, add segment-level error.tsx files wherever a feature deserves a failure message tailored to it. The full recommendation, then: app/error.tsx and app/global-error.tsx at the root, and a segment error.tsx wherever the UX earns one.
Check your understanding
Section titled “Check your understanding”A quick round on the points worth keeping: the two non-negotiables, the dev trap, and the discipline. Mark each claim true or false; the review at the end explains every one.
Each claim is about global-error.tsx and the app's error surface. Mark each statement True or False.
global-error.tsx must render its own <html> and <body> tags.
global-error.tsx is the document — so it has to reconstruct the skeleton itself. Omit the tags and you ship a blank page.global-error.tsx must start with 'use client'.
error.tsx: Error Boundaries are stateful, client-only class machinery. Omit the directive and the build fails.You can verify your global-error.tsx UI by triggering the error in next dev.
global-error.tsx is a good place to fetch the data you need to render a richer error page.
app/global-error.tsx makes app/error.tsx redundant; you only need one.
error.tsx catches under the shell, global-error.tsx catches the shell itself and anything that escapes error.tsx. Ship both at the root.