TL;DR
If app/global-error.tsx never renders while you're running next dev — you just see the red developer error overlay instead — this is documented, intentional behavior, not a broken error boundary. It is confirmed by a 35-reaction GitHub issue against Next.js that many teams open independently before finding out it's by design. Test the fix with next build && next start, not next dev.
-
Symptom:
global-error.tsxnever renders innext dev; you only see the framework's red overlay - Root cause: Next.js deliberately suppresses error/global-error boundaries in development in favor of the overlay, which carries the stack trace and source location the boundary UI can't show
-
Fix: Nothing to patch — verify with a production build (
next build && next start), where boundaries always render -
Gotcha:
global-error.tsxonly catches errors thrown in the Root Layout itself; a regularerror.tsxin a route segment handles everything else
The report that keeps getting filed
The GitHub issue that best describes this is vercel/next.js#55462, "Root Layout Errors not triggering global-error.tsx in dev." The reproduction is simple: throw an error inside app/layout.tsx, run next dev, navigate to the page, and watch for the custom fallback UI defined in app/global-error.tsx. It never shows. Instead, the dev server's overlay — the full-screen red panel with the stack trace — takes over, and it stays that way no matter how the error boundary is written.
Thirty-five reactions on a single issue is a strong signal that this catches experienced teams off guard, not just newcomers. The pattern usually goes: write global-error.tsx in a hurry to satisfy a code-review comment about "handle Root Layout crashes," test it locally, see nothing happen, and file a bug report against Next.js itself before checking whether the framework behaves differently outside dev.
Why the overlay wins in development
global-error.tsx is a special file that can only catch errors thrown by the Root Layout (app/layout.tsx) or Root Template — the one place a normal error.tsx cannot reach, because a segment's error.tsx boundary sits inside the layout it would need to wrap. Next.js's own documentation is explicit that in development, an error thrown in the Root Layout triggers the framework's error overlay instead of rendering global-error.tsx, specifically so you get the component stack, the exact file and line, and a link straight to your editor. A custom fallback UI would replace all of that with whatever <h1>Something went wrong</h1> markup you wrote — strictly worse for debugging the error you're actively causing.
This is not unique to global-error.tsx. The same suppression applies to ordinary error.tsx boundaries in development: Next.js's App Router documentation notes that in development, errors are additionally captured by the built-in overlay so you never lose the stack trace, even though the boundary is technically still mounted underneath it.
The real test: a production build
Because the overlay is a next dev-only feature, the only way to see whether your global-error.tsx actually works is to build for production and run it:
next build
next start
Trigger the same error you were testing in app/layout.tsx. This time, no dev overlay exists to intercept it — global-error.tsx renders exactly as written, including whatever reset() button or Sentry/error-tracking call you wired into it. If it still doesn't render under next start, the bug is in your code, not in the framework's dev-mode behavior, and the checklist below narrows it down.
Three real bugs this can hide behind
Because "test in production only" is inconvenient, it's worth ruling out the actual mistakes that produce the same symptom, since assuming "it's just the dev overlay" can mask a genuine bug:
1. global-error.tsx must define its own <html> and <body>
Unlike every other error boundary, global-error.tsx replaces the Root Layout entirely when it activates — including the <html> tag. If you nest it inside another layout wrapper or forget the tags, the boundary can fail silently in production too:
// app/global-error.tsx
'use client'; // global-error.tsx must be a Client Component
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
);
}
2. It only fires for errors in the Root Layout — not everywhere
global-error.tsx is the boundary of last resort for the one segment nothing else can wrap: the Root Layout. An error thrown inside app/dashboard/page.tsx is caught by app/dashboard/error.tsx (or the nearest ancestor error.tsx), never by global-error.tsx. If you added global-error.tsx expecting it to catch a page-level error, add a regular error.tsx next to that page instead — that one does render its custom UI in development, since it isn't fighting the Root Layout special case.
3. Errors during rendering vs. errors in event handlers
Error boundaries — error.tsx and global-error.tsx alike — only catch errors thrown during rendering, in lifecycle methods, or in constructors of the component tree below them. An error thrown inside a button onClick handler or inside a useEffect callback does not trigger any error boundary; it becomes an unhandled promise rejection or a console error instead. If your test case throws from an event handler, no boundary will ever catch it, in dev or production — that is standard React behavior, not a Next.js quirk.
Verifying you're actually fixed
- Confirm
global-error.tsxlives atapp/global-error.tsx(or inside a route group at the root), defines<html>/<body>, and starts with'use client'. - Run
next build && next start, then trigger the Root Layout error again. The custom fallback UI must render — not the dev overlay, which does not exist in this mode. - Confirm the error genuinely originates during render (throw it directly in the layout body, not inside a handler) if you're using a synthetic test case.
- If you're wiring in error tracking (Sentry, etc.), call
reportError(error)insideglobal-error.tsxand confirm the event lands in your dashboard from the production build — dev-mode testing will never exercise this path.
Related Articles
- Next.js + Supabase: Error Handling & Observability
- Fix Next.js Build Error Module Not Found After Deploy
- Next.js Hydration Mismatch: 8 Fixes for App Router (2026)
- Fix: NextRouter was not mounted
- Fix: component needs useState, no Client parent
Originally published at https://www.iloveblogs.blog
Top comments (0)