Home JavaScript Uncaught Errors in Next.js 16 — Blank White Screen for 15,000 Users
Beginner 6 min · July 12, 2026

Uncaught Errors in Next.js 16 — Blank White Screen for 15,000 Users

How uncaught errors in Next.js 16 crash entire pages.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
Before you start⏱ 20 min
  • React fundamentals
  • Next.js App Router
  • Basic understanding of error boundaries
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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
✦ Definition~90s read
What is Error Handling in Next.js 16?

Next.js error handling is a set of file conventions and React patterns for gracefully handling runtime errors in Server and Client Components. error.tsx creates a React Error Boundary that catches rendering errors at the route segment level, showing a fallback UI instead of a blank page. not-found.tsx handles 404 scenarios — both unmatched routes and intentional notFound() calls from data-fetching components. global-error.tsx is the outermost error boundary that catches errors in the root layout. Together with granular ErrorBoundary wrappers and error logging (Sentry), these conventions ensure that a single component failure doesn't crash the entire application, users see helpful error states, and developers get visibility into production errors.

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.
Plain-English First

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.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

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.

app/dashboard/error.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
'use client'

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  // Log to your error tracking service
  console.error('Dashboard error:', error)

  return (
    <div className="flex h-[400px] items-center justify-center">
      <div className="text-center">
        <h2 className="text-xl font-semibold mb-2">
          Something went wrong
        </h2>
        <p className="text-gray-600 mb-4">
          We couldn't load this section. Please try again.
        </p>
        <button
          onClick={reset}
          className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
        >
          Try again
        </button>
      </div>
    </div>
  )
}
Try it live
error.tsx Must Be a Client Component
Add 'use client' directive. Without it, error.tsx won't catch errors because it needs React's runtime error boundary mechanism.
Production Insight
I audit every Next.js project by grepping for error.tsx. The first time, there are typically zero files across the entire app. After implementing route-level error boundaries and adding Sentry logging, we catch an average of 12 production errors per week that would have otherwise resulted in blank screens.
nextjs-error-handling-guide THECODEFORGE.IO Next.js 16 Error Boundary Hierarchy Layered error handling from root to leaf components Root Layout global-error.js App Layout error.js | not-found.js Route Groups Granular Error Boundaries Page Components Server Components | Client Components Leaf Components Isolated Error Boundaries THECODEFORGE.IO
thecodeforge.io
Nextjs Error Handling Guide

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.

components/SectionErrorBoundary.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
'use client'

import { Component, ReactNode } from 'react'

interface Props {
  children: ReactNode
  fallback?: ReactNode
  name?: string
}

interface State {
  hasError: boolean
  error: Error | null
}

export class SectionErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false, error: null }
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error(`[${this.props.name || 'Section'} error]:`, error)
    // Send to Sentry
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="rounded-lg border border-red-200 bg-red-50 p-4">
          <p className="text-sm text-red-700">
            This section failed to load.
          </p>
        </div>
      )
    }
    return this.props.children
  }
}
Try it live
Granular > Coarse Boundaries
Replace one big error.tsx with multiple ErrorBoundary wrappers around independent sections. A broken sidebar shouldn't crash the main content.
Key Takeaway
Wrap each independent section in its own error boundary. One component's crash should not take down the entire page.

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.

app/not-found.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Root 404 — catches all unmatched routes
export default function NotFound() {
  return (
    <div className="flex min-h-[60vh] items-center justify-center">
      <div className="text-center">
        <h1 className="text-6xl font-bold text-gray-300 mb-4">404</h1>
        <h2 className="text-2xl font-semibold mb-2">
          Page not found
        </h2>
        <p className="text-gray-600 mb-6 max-w-md">
          The page you're looking for doesn't exist or has been moved.
        </p>
        <a
          href="/"
          className="inline-block rounded-lg bg-blue-600 px-6 py-2 text-white hover:bg-blue-700"
        >
          Go home
        </a>
      </div>
    </div>
  )
}

// Usage in a Server Component
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation'

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const product = await getProduct(id)

  if (!product) {
    notFound() // Shows app/products/[id]/not-found.tsx
  }

  return <div>{/* product details */}</div>
}
Try it live
notFound() = Intentional 404
When data doesn't exist, use notFound() for a controlled navigation, not a try-catch. It's cleaner and shows your custom 404.
Server vs Client Component Error Handling Key differences in error propagation and recovery Server Components Client Components Error Boundary Support Not supported directly Supported via error.js Error Propagation Bubbles to nearest error.js Caught by component boundary Recovery Mechanism reset() not available reset() function available Logging Server-side logs only Client-side console logs Fallback UI Uses not-found.js for 404 Uses error.js for errors THECODEFORGE.IO
thecodeforge.io
Nextjs Error Handling Guide

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.

app/global-error.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <div className="flex min-h-screen items-center justify-center">
          <div className="text-center">
            <h1 className="text-2xl font-bold mb-4">
              Critical error
            </h1>
            <p className="text-gray-600 mb-4">
              The application encountered a critical error.
              Our team has been notified.
            </p>
            <button
              onClick={reset}
              className="rounded-lg bg-blue-600 px-6 py-2 text-white"
            >
              Reload page
            </button>
          </div>
        </div>
      </body>
    </html>
  )
}
Try it live
global-error.tsx Needs Its Own HTML Tag
Because it replaces the root layout, you must include <html> and <body> tags in global-error.tsx. It doesn't inherit anything from the layout.

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.

app/dashboard/error.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
'use client'

import * as Sentry from '@sentry/nextjs'
import { usePathname } from 'next/navigation'

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  const pathname = usePathname()

  // Send to Sentry with context
  Sentry.captureException(error, {
    tags: { section: 'dashboard', route: pathname },
    level: 'error',
  })

  return (
    <div className="rounded-lg border border-red-200 bg-red-50 p-6">
      <h2 className="text-lg font-semibold text-red-800 mb-2">
        Dashboard error
      </h2>
      <p className="text-red-600 mb-4">
        We've been notified. Please try again.
      </p>
      <button
        onClick={reset}
        className="rounded-md bg-red-600 px-4 py-2 text-white text-sm"
      >
        Try again
      </button>
    </div>
  )
}
Try it live
Add Context to Every Logged Error
Log the route, section name, and user ID (if available) along with the error. 'Something went wrong on dashboard' is useless. 'Reports section crashed for user 42 on /dashboard/reports' is actionable.

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.

app/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Server Component error
// This throws during SSR — no client-side stack trace
export default async function Page() {
  // If this API call fails, the page crashes silently
  const data = await fetch('https://api.example.com/data')
    .then(r => r.json())

  return <div>{data.name}</div>
  // Error caught by nearest error.tsx or global-error.tsx
}

// Client Component error
// This throws in the browser — visible in console
'use client'

export function Button() {
  const handleClick = () => {
    throw new Error('Client error')
  }

  return <button onClick={handleClick}>Click me</button>
  // Caught by error boundary wrapping this component
}
Try it live
Server Errors Are Silent Killers
Server Component errors never show in the browser console. If you see a blank page with no console errors, look at server logs.
Key Takeaway
Server Component errors require server log inspection. Client Component errors appear in the browser console. Both need error boundaries to prevent blank screens.

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.

app/dashboard/layout.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// This layout overrides the root layout for /dashboard/*
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="grid grid-cols-[250px_1fr]">
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}

// app/dashboard/error.tsx catches layout errors too
// If Sidebar throws, this error.tsx renders instead
// If the layout itself throws (e.g., the grid div), still caught
Try it live
Layouts Need Their Own error.tsx
Add error.tsx at every level that has a layout.tsx. A layout error without a sibling error.tsx crashes all pages under that route.
Key Takeaway
Every layout needs a sibling error.tsx. Root layout needs global-error.tsx. Templates reset error state on navigation.

Error Recovery: The reset() Function and Retry Logic

The reset() 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.

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.

app/dashboard/error.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
'use client'

import { useEffect, useState } from 'react'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  const [retryCount, setRetryCount] = useState(0)

  useEffect(() => {
    // Auto-retry once after 3 seconds for transient errors
    if (retryCount === 0) {
      const timer = setTimeout(() => {
        setRetryCount(1)
        reset()
      }, 3000)
      return () => clearTimeout(timer)
    }
  }, [retryCount, reset])

  return (
    <div>
      <h2>Something went wrong</h2>
      {retryCount >= 1 && (
        <p className="text-sm text-gray-500">
          Auto-retry didn't work. Please try manually.
        </p>
      )}
      <button onClick={() => reset()}>
        {retryCount >= 1 ? 'Retry' : 'Retrying...'}
      </button>
    </div>
  )
}
Try it live
Auto-Retry for Transient Errors
Use setTimeout(() => 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.

app/debug/error-test/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
// A dev-only page to verify error boundaries
// app/debug/error-test/page.tsx
export default function ErrorTestPage() {
  if (process.env.NODE_ENV === 'development') {
    throw new Error('Test error — verify error.tsx renders')
  }

  return <div>This should not render in dev</div>
}

// Visit /debug/error-test in development
// Confirm error.tsx shows with correct styling and reset button
// Then check Sentry (or console) for the captured error
Try it live
Error Boundaries Need Testing Too
An error boundary that doesn't render correctly during an error is worse than no boundary at all — it shows a broken fallback UI.

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.

app/layout.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Minimal root layout — low crash risk
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

// File structure:
// app/
//   global-error.tsx     ← catches root layout crashes
//   not-found.tsx        ← catches 404s
//   layout.tsx           ← minimal, low-crash-risk
//   dashboard/
//     error.tsx          ← catches dashboard errors
//     not-found.tsx       ← catches /dashboard/* notFound()
//     page.tsx
Try it live
The Pyramid of Error Protection
global-error.tsx > layout error.tsx > route error.tsx > granular ErrorBoundaries. Each layer catches what the layer below can't handle.
Key Takeaway
Production error handling: global-error (catastrophic) > route error.tsx (page crashes) > granular ErrorBoundaries (component crashes) > not-found.tsx (missing data). All instrumented with logging.

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.

try-catch in Server Components Can Mask Errors
A try-catch that swallows an error and returns null shifts the problem downstream. Something else will crash on null, and the original error is lost.
Key Takeaway
Don't use try-catch as a replacement for error boundaries. Error boundaries catch rendering errors; try-catch catches data errors. Use both but keep them separate.

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.

components/SuspenseErrorWrapper.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Suspense, ReactNode } from 'react'
import { SectionErrorBoundary } from './SectionErrorBoundary'

interface Props {
  children: ReactNode
  loadingFallback: ReactNode
  errorFallback?: ReactNode
  name?: string
}

export function SuspenseErrorWrapper({
  children,
  loadingFallback,
  errorFallback,
  name,
}: Props) {
  return (
    <SectionErrorBoundary fallback={errorFallback} name={name}>
      <Suspense fallback={loadingFallback}>
        {children}
      </Suspense>
    </SectionErrorBoundary>
  )
}
Try it live
Suspense + ErrorBoundary = Complete Isolation
Suspense handles loading. ErrorBoundary handles failures. Wrap each independent section in both for full isolation.
● Production incidentPOST-MORTEMseverity: high

Production Incident: 15,000 Users Saw a White Screen for 45 Minutes

Symptom
Dashboard route returned a blank white page with no console errors and no network error. The page loaded successfully from the server (no 500), but the React render tree was empty.
Assumption
The team assumed errors in individual components would be isolated by React's error handling. They had no error.js files anywhere in the application.
Root cause
A Server Component in the sidebar tried to access 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.
Fix
Added error.tsx at app/dashboard/ to catch the error and show a degraded dashboard without the sidebar. Fixed the null access with optional chaining: user.preferences?.theme. Added Sentry instrumentation to log all Server Component errors.
Key lesson
  • 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
Production debug guideTrack down every error source and fix it4 entries
Symptom · 01
Blank white page with no errors in console
Fix
Check for uncaught errors in Server Components. Server errors don't show in browser console. Check server logs or add error.tsx to catch them.
Symptom · 02
500 error page on production but not in development
Fix
Development mode shows error overlay with stack trace. Production strips all error details. Add error.tsx with a logging call to capture the error.
Symptom · 03
404 page shows default Next.js not-found
Fix
The route doesn't exist or notFound() was called. Add not-found.tsx at the relevant segment to show a custom 404 page.
Symptom · 04
Error page renders but has no styling
Fix
error.tsx renders inside the layout. If the layout itself crashed, error.tsx may render without styles. Use global-error.tsx for layout-level errors.
★ Quick Debug Reference: Error Boundaries in Next.jsFast commands and checks for diagnosing error handling gaps
Blank white screen
Immediate action
Check if the route has an error.tsx file
Commands
ls app/**/error.tsx 2>/dev/null | head -20
grep -r '\.error' app/ --include='*.tsx' | head -5
Fix now
Create error.tsx in the route segment dir with fallback UI and logging
500 error without details+
Immediate action
Add Sentry or console.error to error.tsx
Commands
grep -r 'error.tsx' app/ --include='*.tsx' | head -10
Check if error.tsx has 'use client' directive
Fix now
error.tsx must be a Client Component. Add console.error(error) or Sentry.captureException(error)
⚙ Quick Reference
9 commands from this guide
FileCommand / CodePurpose
appdashboarderror.tsx'use client'error.js
componentsSectionErrorBoundary.tsx'use client'Granular Error Boundaries
appnot-found.tsxexport default function NotFound() {not-found.js
appglobal-error.tsx'use client'global-error.js
apppage.tsxexport default async function Page() {Server Component Errors vs. Client Component Errors
appdashboardlayout.tsxexport default function DashboardLayout({Handling Errors in Layouts and Templates
appdebugerror-testpage.tsxexport default function ErrorTestPage() {Testing Error Boundaries
applayout.tsxexport const metadata: Metadata = {Complete Error Handling Architecture
componentsSuspenseErrorWrapper.tsxinterface Props {Error Boundaries and Suspense

Key takeaways

1
Every route segment needs error.tsx
without it, an uncaught error produces a blank white screen with no recovery path
2
error.tsx must be a Client Component with 'use client'. It receives error (for debugging) and reset (for retry)
3
global-error.tsx is your last resort for root layout crashes
it needs its own <html> and <body> tags and minimal dependencies
4
not-found.tsx handles both unmatched routes and intentional notFound() calls
always create a branded, helpful 404
5
Pair every error boundary with logging (Sentry, Datadog)
invisible errors are the most dangerous
6
Granular ErrorBoundary wrappers around independent sections prevent one component's crash from taking down the entire page
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain the three error-handling file conventions in Next.js App Router ...
Q02SENIOR
How does React's error boundary mechanism work with Server Components in...
Q03SENIOR
Describe a strategy for implementing comprehensive error handling in a N...
Q04SENIOR
What is the difference between a 404 from an unmatched route and a 404 f...
Q01 of 04JUNIOR

Explain the three error-handling file conventions in Next.js App Router and when each one applies.

ANSWER
The three conventions are error.js, not-found.js, and global-error.js. error.js catches unexpected errors (exceptions thrown during rendering). It must be a Client Component and receives error and reset props. not-found.js handles expected missing resources — triggered by notFound() calls or unmatched routes. It can be a Server or Client Component and receives no props. global-error.js catches errors in the root layout — the one place error.js can't reach. It must define its own <html> and <body> tags. All three can coexist at multiple levels for granular error handling.
FAQ · 7 QUESTIONS

Frequently Asked Questions

01
What's the difference between error.tsx and global-error.tsx?
02
Does error.tsx catch errors in layout.tsx at the same level?
03
Can error.tsx be a Server Component?
04
How do I log errors from error.tsx to a service like Sentry?
05
Should I use try-catch or error boundaries for data fetching errors?
06
What happens if my error.tsx itself throws an error?
07
Can I have multiple error.tsx files in nested routes?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
🔥

That's Next.js. Mark it forged?

6 min read · try the examples if you haven't

Previous
Loading UI, Streaming, and Suspense Boundaries in Next.js 16
27 / 56 · Next.js
Next
Metadata and SEO in Next.js 16: From Basics to Dynamic OG Images