Suggest an editImprove this articleRefine the answer for “Error handling in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Next.js error handling** relies on `error.tsx` files that create a React Error Boundary per route segment. Place one next to `page.tsx` and it catches any runtime error in that segment, renders a fallback UI, and keeps parent layouts intact. Use `notFound()` for missing resources (404), `global-error.tsx` for root layout failures. ```tsx 'use client' export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { return <button onClick={reset}>Retry - ID: {error.digest}</button> } ``` **Key:** `error.tsx` must be a client component; errors bubble up to the nearest parent boundary if no local one exists.Shown above the full answer for quick recall.Answer (EN)Image**Error handling in Next.js App Router** - dedicated files (`error.tsx`, `not-found.tsx`, `global-error.tsx`) create automatic React Error Boundaries at each route segment level, so one failing part doesn't take down the whole app. ## Theory ### TL;DR - Each route segment can have its own `error.tsx` - an error in `/dashboard/users` shows that segment's error UI while the navbar and sidebar stay intact - Think of it like a circuit breaker per floor in a building: one tripped breaker doesn't kill the lights everywhere - `error.tsx` catches runtime crashes; `not-found.tsx` handles missing resources (404s) - `error.tsx` must always be a client component (`'use client'`) - no exceptions - Errors bubble up: if no local `error.tsx` exists, Next.js looks at the parent segment, then root ### Quick example ```tsx // app/dashboard/error.tsx 'use client' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <div> <h2>Dashboard failed to load</h2> <p>{error.message}</p> {/* digest is a hash for server-side logging, safe to display */} <details>Error ID: {error.digest}</details> {/* reset() re-renders only this segment, not the whole app */} <button onClick={reset}>Try again</button> </div> ) } ``` Place this file in `app/dashboard/` and any error thrown inside `/dashboard` or its children triggers this UI. The root layout (navbar, sidebar) stays untouched. ### Key difference `error.tsx` creates a **segment-scoped** boundary. An error in `app/blog/[slug]/page.tsx` only triggers `app/blog/error.tsx` (or bubbles to root), not a white screen. This contrasts with Pages Router, where a crash in one page wiped out the whole shell. One more thing: `error.tsx` does NOT catch errors thrown inside `layout.tsx` of the same segment, because the boundary sits inside the layout. For layout errors, you need `error.tsx` in the parent segment, or `global-error.tsx`. ### When to use - Runtime error in a data fetch or component inside a segment: add `error.tsx` in that folder - Missing resource on a dynamic route (`/posts/123` where the post doesn't exist): call `notFound()` from `next/navigation`, not `throw new Error(...)` - this triggers `not-found.tsx` and returns a proper 404 status - Root layout errors (auth provider crash, theme provider fail): `app/global-error.tsx` - Errors inside Server Actions (form submissions, mutations): return `{ error: '...' }` from the action, don't throw - `error.tsx` doesn't intercept Server Action errors - Errors in API route handlers (`app/api/route.ts`): standard `try/catch` inside the handler, `error.tsx` skips these entirely ### global-error.tsx When an error happens in the root layout itself, `error.tsx` can't help because there's no parent segment to catch it. `global-error.tsx` takes over: ```tsx // app/global-error.tsx 'use client' export default function GlobalError({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( // Must include html + body - this replaces the root layout entirely <html> <body> <h2>Something went wrong</h2> <button onClick={reset}>Reload</button> </body> </html> ) } ``` In development mode, Next.js shows its own error overlay instead of `global-error.tsx`. Your custom component only appears in production builds. ### Bubbling order ``` layout.tsx └── error.tsx (catches errors from children) └── loading.tsx └── not-found.tsx └── page.tsx ``` An error thrown in `page.tsx` bubbles up to the nearest `error.tsx`. If the segment has no `error.tsx`, it goes to the parent's. If nothing catches it at all, the app returns a 500 or `global-error.tsx` fires. ### How it works internally Next.js scans the file system at build/SSR time and wraps each route segment's component tree in a React Error Boundary when `error.tsx` exists. When a throw happens, React's boundary captures it via its `componentDidCatch`-like mechanism and re-renders only that segment's subtree with the error UI. The `reset` function triggers a state update that makes React retry rendering the original component tree without a full navigation. On the server side, `error.digest` is a hash of the stack trace. The client gets this hash but not the actual stack, so sensitive server internals stay private. You forward the digest to your logging service (Sentry, Datadog, Vercel Logs) for tracing. ### Server Actions Server Actions return errors differently from UI components. Because `error.tsx` doesn't intercept them, the pattern is to return an error object: ```tsx 'use server' export async function updateProfile(formData: FormData) { try { await db.user.update({ /* ... */ }) revalidatePath('/settings') return { success: true } } catch (error) { return { error: 'Failed to update profile' } } } ``` The calling component checks the return value and shows its own error state. ### Common mistakes **Forgetting `'use client'` in error.tsx:** ```tsx // Wrong - Next.js will throw a build error export default function Error({ error }: any) { return <div>{error.message}</div> } ``` Error Boundaries require React state management internally. They can't be Server Components. Add `'use client'` at the top, always. **Using `throw new Error(...)` for missing data instead of `notFound()`:** ```tsx // Wrong - shows crash UI, returns 500 status if (!post) throw new Error('Post not found') // Correct - triggers not-found.tsx, returns 404 status import { notFound } from 'next/navigation' if (!post) notFound() ``` A 500 for a missing blog post is bad for SEO. Search engines treat a 404 and a 500 very differently. **Storing retry state around `reset()`:** ```tsx // Problematic - reset() fully re-mounts the segment, local state is gone const [retries, setRetries] = useState(0) <button onClick={() => { setRetries(x => x + 1); reset() }}>Retry</button> ``` `reset()` replaces the segment tree, which destroys all component state. Use `error.digest` for logging; don't try to track retries in local state. **No root error.tsx:** In practice, this is the most common oversight - teams skip root-level error handling until something breaks in production. Always add at least `app/error.tsx` and `app/global-error.tsx` as a safety net. Think of it as the final `catch` block at the application level. **Expecting error.tsx to catch layout errors in the same segment:** ``` app/dashboard/ layout.tsx <- throws here error.tsx <- does NOT catch this ``` The boundary is mounted inside the layout, so it can't catch the layout's own errors. Move `error.tsx` one level up, or handle the error inside `layout.tsx` directly. ### Real-world usage - Vercel Commerce template: segment `error.tsx` for checkout flow crashes, `error.digest` forwarded to Vercel Logs - T3 Stack (tRPC + NextAuth): `app/(auth)/error.tsx` catches tRPC query failures while keeping the app shell - Shadcn/Tremor admin dashboards: `admin/[team]/error.tsx` isolates team data errors per tenant - Payload CMS: custom `error.tsx` that forwards digest to their internal error webhook ### Follow-up questions **Q:** What happens if `error.tsx` itself throws? **A:** It bubbles to the nearest parent `error.tsx`. React has a boundary stacking limit to prevent infinite loops, so eventually it hits `global-error.tsx` or the framework's default handler. **Q:** How does `error.digest` work in production without leaking server details? **A:** Next.js generates a hash of the full stack trace server-side and sends only that hash to the client. You log the hash with your monitoring tool (Sentry, Vercel Logs). The client sees a short ID, not internal stack frames. **Q:** What's the difference between `error.tsx` and wrapping a component in a React `ErrorBoundary` class? **A:** Next.js wraps segments automatically based on file presence. A class-based boundary requires manual placement around every risky component. Also, `reset()` in Next.js re-fetches data via the navigation state, not just a local re-render. **Q:** How do parallel routes affect error bubbling? **A:** An error inside a parallel route slot (like `@modal`) stays in that slot. If the slot has no `error.tsx`, it bubbles to the parent group layout, and `reset()` there retries the whole parallel group including other slots. Add slot-specific `error.tsx` for isolation. **Q:** (Senior) How does Next.js handle server-thrown errors during SSR without leaking the stack to the client? **A:** During RSC rendering, if a throw happens, Next.js renders the error boundary fallback on the server and sends a sanitized RSC payload to the client. The client hydrates with the boundary already in the error state. The digest hash connects client-visible error IDs to full server-side traces in your logging system. ## Examples ### Basic: catch errors in a dynamic product route ```tsx // app/shop/[id]/page.tsx import { notFound } from 'next/navigation' export default async function ProductPage({ params, }: { params: { id: string } }) { const id = parseInt(params.id) // This throw is caught by app/shop/error.tsx if (isNaN(id)) throw new Error('Invalid product ID') const product = await fetchProduct(id) // This triggers not-found.tsx with correct 404 status if (!product) notFound() return <div>{product.name}</div> } ``` ```tsx // app/shop/error.tsx 'use client' export default function ShopError({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <div> <h2>Could not load product</h2> <button onClick={reset}>Try again</button> </div> ) } ``` Visiting `/shop/abc` shows the error UI while the main navigation stays visible. Visiting `/shop/999` (valid ID, no product) shows the not-found page with a 404 status code. ### Intermediate: production error component with Sentry logging ```tsx // app/admin/users/error.tsx 'use client' import { useEffect } from 'react' import * as Sentry from '@sentry/nextjs' export default function AdminUsersError({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { useEffect(() => { // digest ties this client-side log to the full server-side trace in Sentry Sentry.captureException(error, { tags: { segment: 'admin-users', digest: error.digest }, }) }, [error]) return ( <div className="p-8"> <h2 className="text-xl font-bold text-red-600">Admin panel error</h2> <p className="mt-2 text-sm text-gray-500">Error ID: {error.digest}</p> <button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded" > Retry </button> </div> ) } ``` The `useEffect` runs on mount, sending the error to Sentry before the user clicks anything. The `digest` tag in Sentry lets you pull up the full server stack trace from your dashboard. ### Advanced: Server Action with returned error state ```tsx // app/settings/actions.ts 'use server' export async function updateProfile(formData: FormData) { try { const name = formData.get('name') as string await db.user.update({ where: { id: session.userId }, data: { name } }) revalidatePath('/settings') return { success: true } } catch { return { error: 'Failed to update profile. Try again.' } } } ``` ```tsx // app/settings/page.tsx 'use client' import { updateProfile } from './actions' import { useState } from 'react' export default function SettingsForm() { const [errorMsg, setErrorMsg] = useState<string | null>(null) async function handleSubmit(formData: FormData) { const result = await updateProfile(formData) if (result.error) setErrorMsg(result.error) } return ( <form action={handleSubmit}> <input name="name" /> {errorMsg && <p className="text-red-500">{errorMsg}</p>} <button type="submit">Save</button> </form> ) } ``` Server Actions live outside the `error.tsx` boundary. Returning `{ error: '...' }` instead of throwing gives the UI component full control over how to display the problem.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.