Problem
I started seeing this right after teams moved from 16.1.x to 16.2.x: a route that used to fail quietly or flash blank for a moment suddenly showed the new built-in Next.js error UI in production.
That is not just cosmetic. It usually means one of two things:
- the route is throwing before your segment-level
error.tsxcan catch it - the app does not have the error boundary in the place you think it does
The trigger I kept seeing was a redirect-heavy flow. A server component calls an API, branches on the result, and either redirects or throws. Before Next.js 16.2, the failure path was easy to miss. After the 16.2 release on March 18, 2026, the framework shipped a redesigned default error page, so the same bug became visible immediately to users.
If you are upgrading anyway, that is useful. It forces you to fix the route instead of shipping a fragile production flow.
The official release notes call out the change directly in Next.js 16.2: when your app hits an error and you do not have a matching global-error.tsx or error.tsx, Next.js now renders a redesigned built-in fallback.
If your upgrade also touched route transitions, my write-up on Next.js ViewTransition not working in App Router is a useful companion because transition bugs and error-boundary bugs tend to show up in the same release branch.
Why It Happens
The common mistake is assuming every production error will land in the nearest error.tsx. That is not how these failures behave.
I usually see four causes:
1. The error happens above the segment boundary
If the failure happens during layout loading, root data fetching, or a server component tree before the segment mounts, a route-level error.tsx may never get the chance to render. That is where app/global-error.tsx matters.
2. The destination route is failing, not the source route
This is the one that wastes time. A page calls redirect('/target'), the target route throws, and the team keeps debugging the source page because that is where the redirect lives.
3. Redirect logic is mixed with exception logic
I still see handlers doing throw new Error('redirecting') or using generic exceptions as control flow. That becomes noisy fast in App Router because the runtime has to distinguish between expected navigation behavior and actual server failures.
4. The app has a boundary, but in the wrong place
Adding error.tsx beside one page does not help if the crash is happening in a parent layout, another route group, or the root shell.
That is why the new default page shows up more often after 16.2. The bug was already there. The framework just stopped hiding it.
The Fix
I treat this as a boundary audit plus a route audit.
1. Add a real root fallback
If the app does not already have one, add app/global-error.tsx and keep it intentionally boring. Its job is to stop users from hitting the built-in Next.js fallback while you log the real issue and offer a retry.
'use client'
import { useEffect } from 'react'
import type { ErrorInfo } from 'next/error'
export default function GlobalError({ error, unstable_retry }: ErrorInfo) {
useEffect(() => {
console.error('Global App Router error', error)
}, [error])
return (
<html>
<body>
<main className="mx-auto max-w-2xl p-8">
<h1>Something broke in this route tree</h1>
<p>{error.message}</p>
<button onClick={() => unstable_retry()}>Retry</button>
</main>
</body>
</html>
)
}
I prefer unstable_retry() over a dead-end message because it re-runs the fetch path instead of only resetting client state.
2. Stop throwing for navigation
If the branch is a redirect, use redirect() and return early. Do not wrap it in a generic try/catch that converts navigation behavior into a production error.
import { redirect } from 'next/navigation'
export default async function Page() {
const result = await getCheckoutState()
if (result.kind === 'redirect') {
redirect(result.to)
}
if (result.kind === 'error') {
throw new Error(result.message)
}
return <CheckoutPage data={result.data} />
}
That one separation cleans up a surprising number of “why am I seeing the default page?” reports.
3. Check the route that receives the redirect
If /checkout/complete is the real failing page, no amount of editing /checkout/start will help. I usually log both the source branch and the destination render so I can see which route actually threw.
4. Put boundaries where the failures really live
If the risky fetch sits inside app/account/layout.tsx, the fix is usually a boundary or retry strategy around that layout tree, not a child page.
5. Reproduce it in production mode
This part matters. Dev mode is too forgiving for boundary work.
npm run build
npm run start
Then hit the exact failing flow locally. If the route only breaks after build output, you are dealing with a production boundary problem, not a dev overlay problem.
The Practical Rule
When Next.js 16.2 shows the new default error page, do not ask how to hide it first. Ask which part of the route tree is throwing without a matching boundary.
My order is simple:
- add
global-error.tsx - separate redirect logic from error logic
- verify the destination route
- reproduce with
buildandstart
That gives you a stable production fallback and, more importantly, a route tree you can reason about again.
CTA
If your App Router upgrade is technically “done” but production flows are still falling through to framework UI, see my services. I help teams stabilise Next.js release upgrades without shipping the hidden boundary bugs that only show up after deploy.
