Uncaught Errors in Next.js 16 — Blank White Screen for 15,000 Users
How uncaught errors in Next.js 16 crash entire pages.
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
- ✓React fundamentals
- ✓Next.js App Router
- ✓Basic understanding of error boundaries
- An uncaught error in any Server Component crashes the entire page — not just the component — because React unmounts the whole tree
- error.js at the route segment level catches errors and shows a fallback UI, preventing a blank white screen
- not-found.js handles 404s from notFound() calls and unmatched routes, replacing the default '404 This page could not be found'
- global-error.js wraps the root layout for catastrophic errors that crash the app shell itself
- Pair error boundaries with logging (Sentry, Datadog) to catch errors in development before they reach production users
One spilled drink at a party shouldn't shut down the whole venue, but in Next.js without error boundaries, one bad component throws an exception and React tears down the entire page. error.js is like putting a plastic mat under each drink — spills are contained, cleaned up, and the party continues everywhere else.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
It's 2 AM. Your phone buzzes with a PagerDuty alert: "Dashboard endpoint returning 500 for 15,000 users." You open the app. White screen. No error message. No stack trace. Just a blank white void where your dashboard used to be.
The root cause? A third-party analytics widget threw an uncaught TypeError in one of its Server Components. That single error propagated up through React's rendering tree, React unmounted the entire page to prevent rendering an inconsistent UI, and every user on that dashboard route saw nothing.
Next.js 16 has three error-handling file conventions — error.js, not-found.js, and global-error.js — but most teams skip them all, relying on Next.js's default error page. The default shows '500: Internal Server Error' for server errors and a blank screen for client-side rendering crashes. Neither is acceptable for production.
In this guide, you'll build a comprehensive error handling strategy: granular error boundaries at every route segment, custom 404 pages that guide users back to working content, a global error boundary for catastrophic failures, and logging instrumentation that catches errors before they crash your users' sessions.
error.js: The First Line of Defense
error.tsx is a Client Component file that, when placed in a route segment directory (app/dashboard/error.tsx), creates a React Error Boundary that wraps the page and its children. When any component throws during rendering (server-side or client-side), React catches it and renders the error.tsx component instead.
The component receives two props: error (an instance of Error with a message and stack trace in development) and reset (a function that attempts to re-render the segment, useful for transient errors). You must include 'use client' at the top — error boundaries only work in Client Components because they need React runtime error handling.
error.tsx only catches errors in the segment below it. Errors in the root layout are NOT caught by route-level error.tsx — they require global-error.tsx.
Granular Error Boundaries: Isolating Components
A single error.tsx at the route level still replaces the entire page content. For pages with multiple independent sections, wrap each section in its own error boundary using React's ErrorBoundary component from react-error-boundary or a custom wrapper.
This way, if the sidebar crashes, only the sidebar shows an error state. The main content and footer continue working. Users don't lose access to the entire page because of one broken widget.
The pattern: create a reusable <ErrorBoundary fallback={<ErrorFallback />}> wrapper and wrap each independently-crasheable section. For server components, errors propagate up to the nearest Client Component error boundary, so wrap your section in a Client Component that contains the error boundary.
not-found.js: Better Than the Default 404
Next.js's default 404 page says 'This page could not be found' in plain text on a white background. It's the web equivalent of a slammed door. not-found.tsx lets you create a branded, helpful 404 page that guides users back to working content.
not-found.tsx is triggered in two ways: when Next.js can't match a route (/this-doesnt-exist) and when your code calls notFound() from next/navigation. The latter is useful for pages that load dynamic data — if a user visits /products/some-id and the product doesn't exist in your database, call notFound() to show your custom 404 page instead of a broken page.
Unlike error.tsx, not-found.tsx does NOT need the 'use client' directive — it can be a Server or Client Component. It receives no props.
global-error.js: Catching Root Layout Crashes
The root layout (app/layout.tsx) wraps your entire application. If an error occurs in the root layout itself, route-level error.tsx files won't help — they exist INSIDE the layout. The layout is the parent; if it crashes, there's no error boundary above it.
global-error.tsx is the answer. It MUST be placed at app/global-error.tsx, MUST use 'use client', and replaces the entire application shell when the root layout crashes. Think of it as the final safety net.
A critical constraint: global-error.tsx renders outside the root layout, so it doesn't have access to your fonts, global styles, or any providers defined in the layout. You must include its own <html> and <body> tags and import any necessary styles directly. This makes global-error.tsx a self-contained page.
Logging Errors: Making Errors Visible in Production
The biggest problem with error.tsx is that errors become invisible to you. A user sees 'Something went wrong' and maybe clicks 'Try again'. If it works, you never know an error occurred. This is why every error.tsx must log the error to a monitoring service.
Sentry is the standard for Next.js error tracking. Install @sentry/nextjs and configure it in next.config.ts. It automatically captures unhandled exceptions, but you should also manually capture errors in error.tsx boundaries, especially with context about which page and section failed.
For server errors, Sentry's Node.js SDK captures errors during server-side rendering. For client errors, the browser SDK captures runtime exceptions. The combination gives you full visibility into both sides.
Server Component Errors vs. Client Component Errors
Errors in Server Components are caught during server-side rendering. The error is serialized and sent to the client's error.tsx boundary. The error object has a digest property — a hash of the error message used for deduplication in Next.js's logging.
Server errors never reach the browser console because they happen on the server. This makes them especially dangerous — a crashing Server Component silently produces a blank page that you can't debug from DevTools. Always check server logs (next build output and your hosting platform's logs) when debugging blank screens.
Client Component errors happen in the browser after hydration. They appear in the console and are caught by any error boundary wrapping the Client Component. These are easier to debug because the stack trace is available in DevTools.
Handling Errors in Layouts and Templates
Errors in layouts are particularly tricky. A layout wraps all its child pages, so an error in the layout affects every page under that route. If your dashboard layout crashes, every dashboard page shows an error.
The solution: add error.tsx at the same level as the layout. If app/dashboard/layout.tsx throws, app/dashboard/error.tsx catches it. But if the ROOT layout (app/layout.tsx) throws, you need global-error.tsx.
Templates (template.tsx) are like layouts but remount on every navigation. Errors in templates are caught by the parent segment's error.tsx, just like page errors. The difference: templates create a fresh error boundary on each navigation, so a previously-failed template won't stay in an error state when the user navigates to a different page under the same route.
Error Recovery: The reset() Function and Retry Logic
The function in error.tsx attempts to re-render the segment. It's not a page reload — it calls React's error boundary recovery, which tries to render the children again. This works well for transient errors like network timeouts or race conditions.reset()
For persistent errors (bad data, missing config), reset() will fail again and show the error page again. To handle this, implement retry logic with exponential backoff: call reset(), if the error reoccurs within a threshold, show a different message asking the user to reload the page.
You can also auto-retry with a timeout. For example, if the error is likely transient (a network blip), auto-call reset() after 3 seconds. If it fails again, show a manual retry button.
reset(), 3000) in error.tsx for likely-transient errors. Combine with a count to avoid infinite retry loops.Testing Error Boundaries: Don't Ship Blind
Error boundaries are hard to test because they're triggered by exceptions, which you don't want in production. Test them in development by intentionally throwing errors in your components.
Create a <ErrorTest> component that you can toggle in development: if (process.env.NODE_ENV === 'development') throw new Error('Test error'). Place it inside each error boundary to verify the fallback renders correctly.
Unit test your error boundary fallbacks by mocking the error and reset props. Integration test by wrapping a component that throws and asserting the fallback renders. E2E test by visiting routes that intentionally trigger 404s and errors — this is also your SEO audit.
Complete Error Handling Architecture
Here's the production error handling stack: global-error.tsx at the root catches layout crashes. not-found.tsx at the root catches unmatched routes. Each route segment with data fetches has error.tsx with Sentry logging and reset functionality. Each page with independent sections has granular ErrorBoundary wrappers for each section.
Routes that fetch data by ID (e.g., /products/[id]) call notFound() when the data doesn't exist, and have a segment-level not-found.tsx for a branded 404. The root layout itself has minimal logic (just html/body/fonts) to minimize the chance of it crashing.
This architecture guarantees that no single error can take down more than its own section. A sidebar crash shows 'sidebar unavailable' text. A product 404 shows the branded 404. A root layout error shows a minimal branded crash page. Users always have a path back to working content.
Common Pitfalls and How to Avoid Them
Most teams make the same mistakes. First: error.tsx without logging. You catch the error but never know it happened. Second: relying on try-catch instead of error boundaries. try-catch inside a Server Component prevents the error from propagating to error.tsx, but the page still fails silently — the caught error returns undefined, which causes a different error downstream.
Third: forgetting that error.tsx is a Client Component. I've seen teams spend hours debugging why their error.tsx doesn't catch server errors — it's because they didn't add 'use client'. Fourth: putting too much logic in the root layout. A crash in root layout requires global-error.tsx, which is a self-contained page without styles. Keep the root layout as thin as possible.
Fifth: not testing error pages. Deploy error.tsx and never verify it renders correctly. When a real error occurs, the fallback itself is broken. Always test by intentionally triggering errors in development.
Error Boundaries and Suspense: Working Together
Error boundaries and Suspense boundaries can nest inside each other. When a Suspense boundary is inside an error boundary, a failed data fetch inside Suspense triggers the error boundary. When an error boundary is inside a Suspense boundary, an error in the error boundary's fallback also gets caught? No — the error boundary's fallback is NOT wrapped by the same boundary. If your error boundary fallback crashes, it propagates up to the parent error boundary.
Best practice: wrap each Suspense boundary in its own error boundary. This pattern isolates both loading states and error states per section. A slow section shows a skeleton; a failed section shows an error message; both are independent of other sections.
Production Incident: 15,000 Users Saw a White Screen for 45 Minutes
user.preferences.theme where preferences was null for users who had never opened settings. React's error boundary on the server caught this during rendering and, because there was no error.js, React unmounted the entire RSC tree, sending an empty shell to the client.user.preferences?.theme. Added Sentry instrumentation to log all Server Component errors.- Every route segment needs an error.tsx — even if it's just a generic 'Something went wrong' message
- Null reference errors in deeply nested components are silent page-killers without error boundaries
- Optional chaining (?.) is not optional — always assume nested object paths can be null
- Instrument error.tsx with logging to get visibility into errors that would otherwise be invisible
ls app/**/error.tsx 2>/dev/null | head -20grep -r '\.error' app/ --include='*.tsx' | head -5| File | Command / Code | Purpose |
|---|---|---|
| app | 'use client' | error.js |
| components | 'use client' | Granular Error Boundaries |
| app | export default function NotFound() { | not-found.js |
| app | 'use client' | global-error.js |
| app | export default async function Page() { | Server Component Errors vs. Client Component Errors |
| app | export default function DashboardLayout({ | Handling Errors in Layouts and Templates |
| app | export default function ErrorTestPage() { | Testing Error Boundaries |
| app | export const metadata: Metadata = { | Complete Error Handling Architecture |
| components | interface Props { | Error Boundaries and Suspense |
Key takeaways
Interview Questions on This Topic
Explain the three error-handling file conventions in Next.js App Router and when each one applies.
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
That's Next.js. Mark it forged?
6 min read · try the examples if you haven't