Module A-17·24 min read

The complete error hierarchy: segment vs page vs root error boundaries, global-error.tsx for root layout crashes, the digest prop for server-side error correlation, how errors propagate through nested RSC trees, Suspense + error boundary interaction, and the four error categories (validation, not-found, auth, unexpected) with the right handling pattern for each.

A-17 — Error Architecture and Recovery Patterns

Who this is for: Engineers who know error.tsx exists and want the complete picture before a production incident forces them to learn it the hard way.

Next.js has four distinct error handling mechanisms. Most developers know one of them. Using the wrong mechanism for the wrong error category produces two failure modes: swallowed errors (the user sees nothing, you debug nothing) or nuclear 500s for recoverable situations that should have been inline validation messages. This module gives you the complete map.


The Four Error Categories

Before touching any code, classify your errors. The classification drives everything else.

CategoryExampleCorrect mechanism
Validation error"Email already taken", "Price must be positive"Return from Server Action, display inline
Not-foundProduct slug doesn't exist, deleted recordnotFound()not-found.tsx
Auth error"You don't have permission to view this"redirect('/login') or forbidden()
Unexpected errorDB timeout, null dereference, third-party API crashthrow → caught by error.tsx

The rule: anything a user can cause by providing bad input or lacking permissions should never reach error.tsx. Those are expected outcomes of a working system. Only genuinely unexpected failures — things the code should never encounter under normal operation — belong in error.tsx.

Mixing these up is the root cause of most Next.js error handling bugs:

  • Throwing on validation → user sees "Something went wrong" instead of "Email already taken"
  • Returning on DB timeout → error is silently swallowed, user sees a form that never submits
  • Using notFound() for auth errors → crawler sees a 404 instead of a 401, affecting SEO
  • Using error.tsx for not-found → you lose the 404 status code semantics entirely

error.tsx — Segment-Level Error Boundaries

error.tsx is a React error boundary implemented as a Next.js file convention. It catches unexpected errors thrown from Server Components, Client Components, and Server Actions within the same route segment and all child segments that don't have their own error.tsx.

Under the hood, it wraps the route segment's content in a class-based React error boundary. You write a function component; Next.js handles the class wrapper.

typescript

Two props are passed:

error — the Error object. In production, error.message is a generic string ("An error occurred in the Server Components render") — not the original error message. Next.js deliberately strips error details from the client-side error to prevent information leakage. The error.digest is what connects the client-visible error to your server logs.

reset — a function that triggers a re-render of the error boundary's children. More on this below.

The 'use client' directive is not optional. Error boundaries are fundamentally about rendering fallback UI when a component fails to render. Detecting that failure requires React's componentDidCatch lifecycle, which only runs in the browser. Server Components cannot be error boundaries.

The digest — Your Incident Correlation Token

When Next.js catches an error server-side, it:

  1. Generates a deterministic hash (the digest) from the error details.
  2. Logs the full error (stack trace, request context) server-side, tagged with the digest.
  3. Sends only the digest to the client.

The user sees digest: "1234abcd". Your server logs contain the full stack trace tagged digest=1234abcd. You search your logging platform for that digest and find the exact error, the exact component, the exact line.

This is the intended workflow. Show the digest in your error UI. Train your support team to ask for it. Search for it in your observability platform when debugging.

typescript

global-error.tsx — Root Layout Crashes

error.tsx has a blind spot: it cannot catch errors thrown by its sibling layout.tsx.

The segment hierarchy matters here. error.tsx at app/error.tsx wraps the page content but lives inside the root layout. If the root layout throws — database connection failure during session initialisation, a crash in your analytics provider — the root layout never finishes rendering. The error boundary inside the layout never gets mounted. Nothing catches the error.

global-error.tsx handles this case:

typescript

global-error.tsx is the only Next.js component that must render <html> and <body> tags. It replaces the entire document — not a segment within the layout, but the full HTML output. Your root layout is not available. Your design system is not available. Your fonts are not loaded. This is a bare-bones document.

Keep global-error.tsx minimal. Its job is to tell the user something is catastrophically wrong and give them a reference number for support. It is not a full page — it's an emergency fallback.

In development, Next.js shows a full-page error overlay and ignores global-error.tsx. Test it by running a production build (next build && next start) and intentionally throwing from your root layout.


Error Propagation Through the RSC Tree

Errors propagate upward through the component tree until they hit an error boundary. The nearest ancestor error.tsx catches the error.

Understanding the exact propagation path prevents surprises:

text

The critical rule: a layout's errors are caught by the parent segment's error boundary, not its own segment's error boundary.

This means if you put all your data fetching in layout.tsx (a common pattern for sharing data between page and child routes), an error in that fetch takes down the nearest parent error boundary — potentially a large portion of your UI. It's a reason to prefer page-level data fetching or segment-specific error boundaries close to the data they protect.

Adding error.tsx files deeper in the tree narrows the blast radius of errors. A crash in dashboard/analytics/page.tsx with a dashboard/analytics/error.tsx in place only takes down the analytics section. Without it, the crash propagates to dashboard/error.tsx and takes down the entire dashboard.


Errors Inside Suspense Boundaries

A common misconception: Suspense boundaries are error boundaries.

They are not.

typescript

<SlowAnalytics /> throws. The Suspense boundary's loading state disappears. The error propagates out of the Suspense boundary up to the nearest error.tsx — in this case dashboard/error.tsx or higher. The entire dashboard segment shows the error UI, not just the analytics section.

If you want per-component error recovery that doesn't take down the entire segment, you need a React error boundary wrapping the component inside the Suspense boundary. The react-error-boundary package provides a clean component for this:

typescript
typescript

The <SafeAnalytics> component is Client Component (all React error boundaries are). The <SlowAnalytics> component can still be a Server Component — Client Components can render Server Components as children. The error boundary catches the error without converting SlowAnalytics to a Client Component.

When to use this pattern: any component that fetches non-critical data for a section of a page. Analytics, recommendations, secondary feeds. Errors in these should degrade the section, not the page.

When NOT to use this pattern: primary content. If the main content of a page fails to load, that IS a page error and error.tsx is the right mechanism.


Server Action Errors — Return, Don't Throw

Server Actions that throw unhandled exceptions trigger the nearest error.tsx. This is correct for unexpected failures. It is catastrophically wrong for expected business logic failures.

If a user submits a form with a duplicate email address and your Server Action throws, the user sees a full-page "Something went wrong" screen. Their form data is gone. They don't know what happened. This is not an acceptable user experience.

Expected failures should be returned as data:

typescript

The caller in the Client Component handles all returned failure states inline:

typescript

The pattern: return expected failure states (anything a user or external system can cause), throw unexpected ones (bugs, infrastructure failures). This keeps error.tsx for genuine crashes and keeps users informed for recoverable situations.


The reset() Function and When It Works

reset() in error.tsx triggers a re-render attempt of the error boundary's children. It does not reload the page. It does not clear React state outside the error boundary. It calls React's ErrorBoundary.reset() under the hood, which unmounts and remounts the failed subtree.

reset() works when the error was transient: a network request that timed out but would succeed on retry, a rate limit that has since cleared, a temporary dependency that has recovered.

reset() does not work when the error is persistent: a component with a null dereference bug, missing required data, a broken API contract. In these cases, reset() re-renders the failed component, which throws the same error immediately. The user sees the error UI reappear. After enough retries, it becomes obvious something structural is wrong.

Implement a retry counter that degrades to a page reload after multiple failures:

typescript

Two retries before reloading is a reasonable default. Transient errors (network timeouts) usually resolve on the first retry. If two retries have failed, the error is likely persistent and a full page reload is the appropriate recovery action.


not-found.tsx and the notFound() Function

notFound() is a special-case error. When any Server Component calls notFound(), Next.js stops rendering that component, returns HTTP 404, and renders the nearest not-found.tsx in the tree.

typescript
typescript

notFound() throws a special error internally. Like redirect(), it should not be inside a try/catch — the catch block would swallow it:

typescript
typescript

The hierarchy for not-found.tsx is the same as error.tsx: the nearest ancestor not-found.tsx catches the notFound() call. Add a root-level app/not-found.tsx as the global fallback.


Connecting Errors to Your Observability Stack

The digest system gives you a correlation token. To make it useful, you need to capture errors server-side in a way that preserves the digest.

Next.js integrates with OpenTelemetry through instrumentation.ts. Sentry's Next.js SDK instruments this automatically:

typescript

The onRequestError export is a Next.js hook introduced in Next.js 15 that fires for every request error before it reaches error.tsx. Sentry captures it here along with the digest, so when a user reports digest: "1234abcd", you can search Sentry and find the full stack trace, the request URL, the user's session, and the deployment version.

For manual error tracking without Sentry:

typescript

This fires server-side before the error reaches the client. The digest in your error tracking database matches the digest shown to users.


The Complete Error Handling Decision Tree

When writing code that might fail, run through this:

  1. Is this failure caused by invalid user input? → Return { success: false, error: "message" } from the Server Action. Display inline.

  2. Is this failure caused by the user lacking access? → Return with code: 'AUTH', redirect to login. Or call forbidden() (Next.js 15+) for API-like 403 responses.

  3. Is this failure because a requested resource doesn't exist? → Call notFound(). This gives the correct 404 HTTP status and renders not-found.tsx.

  4. Is this failure something the user cannot resolve? → Let it throw. It propagates to the nearest error.tsx and shows a user-friendly fallback with a digest for support.

  5. Could this failure take down the root layout? → Make sure global-error.tsx exists and handles it gracefully.

Every error in your application falls into one of these categories. The category determines the mechanism. The mechanism determines the user experience.


Where We Go From Here

A-18 covers middleware architecture — the execution model, the limitations of the Edge Runtime in middleware, authentication patterns, and the problems that arise when middleware and Server Actions make conflicting auth decisions. With A-17's error propagation model understood, A-18's middleware error handling (middleware cannot use error.tsx) will make immediate sense.


Knowledge Check

According to the error classification table, what is the correct mechanism for handling a validation error, such as "Email already taken"?


Why must the global-error.tsx component include <html> and <body> tags?


How should the notFound() function be used correctly when fetching data?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.