๐Ÿณ IntuitiveFE
Login
โ† All concepts

Next.js Error Handling

โฑ๏ธ ~4-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿ”ฅ error.tsx = fire doors per floor. ๐Ÿ” not-found.tsx = lost & found desk. ๐Ÿšจ global-error.tsx = building evacuation.

The fire safety analogy

error.tsx (fire doors per floor): each floor has its own fire door that contains smoke to that floor. A fire on the 3rd floor doesn't evacuate the whole building โ€” only the 3rd floor. Each route segment can have its own error.tsx, containing failures to that section.

not-found.tsx (lost & found desk): when something isn't where it's supposed to be, you go to lost & found โ€” a specific, calm place for missing items. not-found.tsx is the polite "this doesn't exist" UI, not an error state.

global-error.tsx (building evacuation): when the fire is in the building's core โ€” the root layout โ€” you evacuate everyone. global-error.tsx replaces the entire app with a last-resort UI. It's the alarm that fires when everything else fails.

Error boundary hierarchy

js
1ย 
2 app/
3 โ”œโ”€โ”€ global-error.tsx โ† catches errors in layout.tsx (root)
4 โ”œโ”€โ”€ layout.tsx โ† ROOT: global-error.tsx catches this
5 โ”œโ”€โ”€ not-found.tsx โ† catches notFound() anywhere in app
6 โ”‚
7 โ”œโ”€โ”€ dashboard/
8 โ”‚ โ”œโ”€โ”€ error.tsx โ† catches render errors in dashboard segment
9 โ”‚ โ”œโ”€โ”€ layout.tsx โ† if THIS throws โ†’ parent error.tsx catches it
10 โ”‚ โ””โ”€โ”€ page.tsx โ† if THIS throws โ†’ dashboard/error.tsx catches it
11 โ”‚
12 โ””โ”€โ”€ blog/
13 โ”œโ”€โ”€ error.tsx โ† catches render errors in blog segment
14 โ”œโ”€โ”€ not-found.tsx โ† catches notFound() inside /blog/**
15 โ””โ”€โ”€ [slug]/
16 โ””โ”€โ”€ page.tsx โ† notFound() โ†’ blog/not-found.tsx
17 โ† throw Error โ†’ blog/error.tsx
18ย 
19Key rule: error.tsx catches errors from its CHILDREN, not from
20layout.tsx at the SAME level. To catch layout errors, put
21error.tsx in the PARENT directory.

Render errors (use error.tsx)

  • โ€ข throw in a Server Component
  • โ€ข Async component Promise rejection
  • โ€ข Errors in child components during render
  • NOT: event handler errors

Expected errors (return, don't throw)

  • โ€ข Validation failures in Server Actions
  • โ€ข DB record not found (could use notFound())
  • โ€ข Network timeouts in Server Actions
  • Return { error: string } instead

The most important rule

error.tsx does NOT catch errors in layout.tsx at the same level. A dashboard/error.tsx catches errors in dashboard/page.tsx, but NOT in dashboard/layout.tsx. To catch layout errors, place error.tsx in the parent directory (or use global-error.tsx for the root).

๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.
Catches:Errors thrown in the route segment (layout + page children)
Triggered by:Any unhandled throw during rendering of the segment

global-error.tsx

Catches root layout errors

โ””

app/error.tsx

Catches app-level segment errors

โ””

not-found.tsx

Catches notFound() calls

โ””

app/dashboard/error.tsx

Catches dashboard segment errors

โ””

reset() handler

Retries the failed boundary

โ””

Server Action try/catch

Returns { error } โ€” no boundary involved

โš  Gotcha: error.tsx does NOT catch errors thrown by layout.tsx at the same level โ€” it only wraps page.tsx and its children. To catch layout errors, place error.tsx in the parent directory.
๐Ÿ’ก Insight: error.tsx implements a React Error Boundary. It's rendered when a rendering error escapes from the route segment. The error prop's 'digest' field links the client-side error to your server logs.
Explored:๐Ÿ”ฅ๐Ÿ”๐Ÿšจโšก๐Ÿ”„
๐ŸŽฏ

Challenge

Which file catches an error thrown inside the root layout.tsx?

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview Q&A

Q: Why use error.tsx instead of try/catch in components?

A: error.tsx is a React Error Boundary โ€” it catches rendering errors thrown during the render phase, which try/catch can't. For async errors in event handlers, use try/catch. For render errors, use error.tsx.

js
1// โœ— Can't catch render-phase errors with try/catch:
2function MyComponent() {
3 try {
4 return <UserProfile />; // if UserProfile throws during render,
5 // the throw propagates PAST this try/catch
6 } catch (e) {
7 // This won't catch render errors โ€” React's fiber reconciler
8 // propagates render errors up through Error Boundaries, not
9 // through the normal JS call stack try/catch
10 }
11}
12ย 
13// โœ“ error.tsx implements an Error Boundary correctly:
14// React catches the thrown value during reconciliation
15// and renders the error.tsx fallback instead
16ย 
17// โœ“ DO use try/catch for event handlers (not render errors):
18function MyButton() {
19 async function handleClick() {
20 try {
21 await someAsyncOperation(); // event handler โ€” try/catch works
22 } catch (e) {
23 setError(e.message); // store in state to display
24 }
25 }
26 return <button onClick={handleClick}>Click</button>;
27}

Q: When does not-found.tsx render?

A: When you call notFound() from any Server Component or Route Handler in a segment, or when Next.js can't match the URL to any route. It renders the nearest not-found.tsx up the tree.

js
1// Two triggers for not-found.tsx:
2ย 
3// 1. Explicit: calling notFound() in your code
4import { notFound } from "next/navigation";
5ย 
6export default async function ProductPage({ params }) {
7 const product = await db.product.findById(params.id);
8 if (!product) notFound(); // โ†’ renders nearest not-found.tsx
9 return <Product data={product} />;
10}
11ย 
12// 2. Implicit: Next.js can't match the URL to any route
13// User visits /products/abc/xyz/nonexistent
14// โ†’ no matching page.tsx anywhere in the route tree
15// โ†’ Next.js calls notFound() internally
16// โ†’ renders app/not-found.tsx (root level)
17ย 
18// Nearest not-found.tsx wins:
19// /products/123 โ†’ not found โ†’ checks:
20// app/products/[id]/not-found.tsx (if exists)
21// app/products/not-found.tsx (if exists)
22// app/not-found.tsx (root fallback)
23// First one found up the tree is rendered

Q: Why must global-error.tsx be a Client Component?

A: It replaces the entire root layout, including the html and body tags โ€” it must render the full document. Since it needs to call reset(), and reset is a function prop, it requires 'use client'.

ts
1// global-error.tsx MUST be "use client" because:
2// 1. React Error Boundaries are a client-side React feature
3// (Error Boundaries don't exist in RSC rendering)
4// 2. It receives a 'reset' function prop โ€” functions can only
5// be passed to Client Components
6// 3. It renders <html> and <body> โ€” it's a full document replacement
7ย 
8// global-error.tsx โ€” correct
9"use client"; // REQUIRED โ€” not optional
10ย 
11export default function GlobalError({
12 error,
13 reset,
14}: {
15 error: Error & { digest?: string };
16 reset: () => void; // โ† function prop โ†’ must be Client Component
17}) {
18 return (
19 <html> {/* Must render html+body โ€” root layout is gone */}
20 <body>
21 <button onClick={reset}>Try again</button>
22 </body>
23 </html>
24 );
25}
26ย 
27// Without "use client" โ†’ build error:
28// Error: global-error.tsx must be a Client Component
๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

error.tsx props: error.digest and error serialization

The error prop received by error.tsx is not the original server error. Next.js serializes errors from Server Components before sending them to the client. In production, only the message is sent โ€” the stack trace is stripped. The digest is a short hash that links the client-side error to the full server-side stack trace in your logs.

js
1// Server Component (runs on server):
2export default async function ServerPage() {
3 throw new Error("DB connection failed: PostgreSQL ECONNREFUSED");
4}
5ย 
6// What error.tsx receives in PRODUCTION:
7{
8 message: "An error occurred in the Server Components render.",
9 // ^ Generic message โ€” real message NOT sent to client for security
10 digest: "a1b2c3d4",
11 // ^ Hash you can grep in your server logs to find the real error
12}
13ย 
14// What error.tsx receives in DEVELOPMENT:
15{
16 message: "DB connection failed: PostgreSQL ECONNREFUSED",
17 // ^ Full message โ€” dev mode sends the real error for debugging
18 stack: "Error: DB connection failed\n at ServerPage ...",
19 digest: "a1b2c3d4",
20}
21ย 
22// Best practice: log digest to your error tracker
23useEffect(() => {
24 if (error.digest) {
25 Sentry.captureException(error, { tags: { digest: error.digest } });
26 }
27}, [error]);
28// โ†’ Server logs show: "Error digest: a1b2c3d4" with full stack

Suspense + Error Boundary interaction

When an async Server Component inside a <Suspense> boundary throws, the error propagates up through the Suspense boundary to the nearest Error Boundary (error.tsx). The fallback prop of Suspense is only shown during loading โ€” on error, it's the Error Boundary that takes over.

js
1// Layout structure:
2// app/dashboard/
3// error.tsx โ† Error Boundary wraps this segment
4// layout.tsx
5// page.tsx
6ย 
7// page.tsx:
8import { Suspense } from "react";
9ย 
10export default function DashboardPage() {
11 return (
12 <div>
13 <StaticHeader />
14 <Suspense fallback={<Spinner />}>
15 {/* AsyncWidget suspends (shows Spinner) while loading */}
16 {/* If AsyncWidget THROWS, Suspense does NOT catch it */}
17 {/* The throw propagates to error.tsx */}
18 <AsyncWidget />
19 </Suspense>
20 </div>
21 );
22}
23ย 
24async function AsyncWidget() {
25 const data = await fetchData(); // Promise rejection โ†’ throws
26 return <Widget data={data} />;
27}
28ย 
29// Error flow:
30// 1. AsyncWidget starts rendering โ†’ suspends (Spinner shows)
31// 2. fetchData() rejects โ†’ AsyncWidget throws
32// 3. Suspense does NOT catch throws โ€” only suspends (pending state)
33// 4. Error propagates to nearest Error Boundary โ†’ error.tsx renders

Custom error classes and structured error handling

ts
1// lib/errors.ts โ€” structured error types
2export class AppError extends Error {
3 constructor(
4 message: string,
5 public code: string,
6 public statusCode: number = 500
7 ) {
8 super(message);
9 this.name = "AppError";
10 }
11}
12ย 
13export class ValidationError extends AppError {
14 constructor(message: string, public fields: Record<string, string[]>) {
15 super(message, "VALIDATION_ERROR", 422);
16 this.name = "ValidationError";
17 }
18}
19ย 
20export class NotFoundError extends AppError {
21 constructor(resource: string) {
22 super(`${resource} not found`, "NOT_FOUND", 404);
23 this.name = "NotFoundError";
24 }
25}
26ย 
27// Server Component โ€” throw typed errors
28import { NotFoundError } from "@/lib/errors";
29ย 
30export default async function ProductPage({ params }) {
31 const product = await db.product.findById(params.id);
32 if (!product) throw new NotFoundError("Product");
33 return <Product data={product} />;
34}
35ย 
36// error.tsx โ€” handle typed errors
37"use client";
38export default function ProductError({ error, reset }) {
39 // In dev, you can check error.name โ€” in prod, use error.digest
40 const isNotFound = error.message?.includes("not found");
41 return (
42 <div>
43 {isNotFound ? (
44 <p>This product doesn't exist. <a href="/products">Browse all</a></p>
45 ) : (
46 <button onClick={reset}>Try again</button>
47 )}
48 </div>
49 );
50}

Using error.digest for server-side logging correlation

Next.js generates a digest for each Server Component error. This digest appears in both the server log and the client error prop, letting you correlate client reports with server stack traces.

js
1// Server-side: Next.js logs the error with its digest
2// Console output (server):
3// Error: DB connection failed [digest: "a1b2c3d4"]
4// at ProductPage (app/products/[id]/page.tsx:12:7)
5// at async ...
6ย 
7// Client-side: error.tsx receives the digest
8"use client";
9export default function ProductError({ error, reset }) {
10 // Log to client-side error tracking
11 // Include digest so support can find the server log
12 useEffect(() => {
13 fetch("/api/log-error", {
14 method: "POST",
15 body: JSON.stringify({
16 digest: error.digest,
17 userAgent: navigator.userAgent,
18 url: window.location.href,
19 }),
20 });
21 }, [error]);
22ย 
23 return (
24 <div>
25 <p>Something went wrong.</p>
26 {error.digest && (
27 <p className="text-xs text-gray-400">
28 Reference: {error.digest}
29 {/* User can give this to support โ†’ support greps server logs */}
30 </p>
31 )}
32 <button onClick={reset}>Try again</button>
33 </div>
34 );
35}
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

It doesn't catch: layout.tsx errors at the same level, event handler errors, or Server Action throws.

React Error Boundaries require the client runtime โ€” they're class-based and receive a reset() function prop.

A server-generated hash that links the client-visible error to the full server-side stack trace in your logs.

reset() alone for transient errors; router.refresh() first for data errors so the retry gets fresh server data.

Suspense only handles pending state โ€” errors thrown inside Suspense propagate up to the nearest Error Boundary (error.tsx).

notFound() is a signal, not an error โ€” it renders not-found.tsx with a 404 status without triggering error.tsx.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What is the correct approach to show per-field validation errors from a Server Action in a form?