error.tsx wraps page.tsx and child layouts in a React error boundary but does NOT wrap the layout.tsx in the same route segment, so errors thrown inside your root layout are not caught by a root-level error.tsx. global-error.tsx exists specifically to catch those root layout errors and replaces the entire root layout when it renders, which means it must include its own html and body tags. For most applications, error.tsx at multiple levels of the route hierarchy covers the majority of cases, with global-error.tsx serving as a last-resort fallback.
In production, Next.js sanitizes server-side error messages before passing them to the Client Component error boundary to prevent leaking sensitive server information to users. The detailed message and stack trace you see in development are stripped, leaving only a generic placeholder. Relying on error.message in your error.tsx UI will silently show nothing meaningful in production, so the component should show a fixed, user-friendly message instead.
The reset prop calls React's error boundary reset, which attempts to re-render the boundary's children without a full page navigation. For transient errors like a brief network outage or temporary API unavailability, this can succeed and restore the UI without the user losing their place. However, reset will not fix a persistent error caused by bad data or a code bug — the boundary will catch the error again immediately, so a secondary 'Go home' link is also recommended.
error.tsx runs client-side and has no access to server-side logging, so server errors that triggered the boundary are not automatically forwarded to Sentry's server integration. The recommended approach is to use the onRequestError instrumentation hook (available in Next.js 15) inside instrumentation.ts to centrally capture all unhandled Server Component errors on the server, while using Sentry.captureException inside error.tsx to capture client-side errors. This two-path setup ensures every error — regardless of where it originated — is reported with proper context.
Calling notFound() is the right choice whenever a resource with the requested identifier does not exist in the database — for example, a blog post or product with an unknown slug. It triggers not-found.tsx, giving users a proper 404 experience with navigation options rather than a broken or empty page caused by rendering null data. A well-customized not-found.tsx with site navigation prevents users from hitting a dead end.
Error handling is the feature developers consistently under-invest in until a production incident teaches the lesson. Next.js App Router introduced a file-based error boundary system that aligns error handling with your route hierarchy — a major improvement over the Pages Router where you'd set up error boundaries manually. I've built robust error handling into multiple production Next.js applications and the pattern I'm sharing is what actually works in the field.
Next.js App Router has three error handling files: error.tsx catches runtime errors in the route segment and its children. not-found.tsx catches notFound() calls and 404 routes. global-error.tsx catches errors in the root layout (including layout.tsx itself). error.tsx is a Client Component (must have 'use client') and receives an error prop (the thrown Error) and a reset prop (a function to retry rendering). Errors bubble up through the hierarchy — an error in a nested route segment will be caught by the nearest error.tsx above it.
error.tsx wraps page.tsx, loading.tsx, and child layouts in a React error boundary, but does NOT wrap the layout.tsx in the same segment. This means errors in your root layout won't be caught by a root-level error.tsx — you need global-error.tsx for that. global-error.tsx replaces the entire root layout when it renders, so it must include <html> and <body> tags. For most applications, the important boundary is the route segment error.tsx — global-error.tsx is a last-resort fallback.
Next.js App Router error boundary hierarchy:
global-error.tsx ← catches errors in root layout.tsx (last resort)
└── app/layout.tsx
└── app/[locale]/layout.tsx
├── error.tsx ← catches errors in this route segment & children
│ └── app/[locale]/(pages)/layout.tsx
│ ├── error.tsx ← more specific boundary
│ │ ├── loading.tsx
│ │ ├── not-found.tsx ← catches notFound() calls
│ │ └── page.tsx ← your page component
│ └── ...
└── ...
Error file reference:
┌─────────────────┬────────────────────────────────────────────────┐
│ File │ When it renders │
├─────────────────┼────────────────────────────────────────────────┤
│ error.tsx │ Runtime error in page.tsx, loading, child layout│
│ not-found.tsx │ notFound() called or no route match │
│ global-error.tsx│ Error in root layout.tsx (must include html+body)│
└─────────────────┴────────────────────────────────────────────────┘
Data flow for Server Component errors:
Server Component throws → Next.js catches → sanitizes in prod
→ passes minimal info to nearest error.tsx (Client Component)
→ error.tsx renders fallback UI + reset buttonFrom building error handling for an ERP dashboard: nest error.tsx at multiple levels of your route hierarchy, not just at the root. A critical accounting feature that errors should show a targeted message about that feature — not a generic app-level error. Put error.tsx in your dashboard layout, in your specific feature segments, and keep a root error.tsx as the final catch-all. Each level gives users more specific recovery options.
Next.js handles server-side errors differently from client-side errors. A Server Component that throws an error during rendering causes Next.js to pass the error information to the nearest error.tsx Client Component. In development, you see the full error message and stack trace. In production, the error message is sanitized — Next.js shows a generic message to prevent leaking sensitive server information. This means your error.tsx shouldn't try to display error.message in production — the server-side details won't be there.
// app/[locale]/(pages)/error.tsx
"use client"
import { useEffect } from "react"
import * as Sentry from "@sentry/nextjs"
interface Props {
error: Error & { digest?: string }
reset: () => void
}
export default function Error({ error, reset }: Props) {
useEffect(() => {
// Capture client-side error (error boundary was activated client-side)
Sentry.captureException(error)
}, [error])
return (
<div className="flex flex-col items-center justify-center min-h-[400px] gap-6">
<div className="text-center">
<h2 className="text-2xl font-bold text-slate-100 mb-2">Something went wrong</h2>
<p className="text-slate-400 text-sm max-w-md">
{/* In production, error.message is sanitized for Server Component errors */}
An unexpected error occurred. Please try again or contact support
{error.digest ? ` (Error ID: ${error.digest})` : ""}.
</p>
</div>
<div className="flex gap-3">
<button onClick={reset} className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
Try again
</button>
<a href="/" className="px-4 py-2 border border-slate-600 text-slate-300 rounded-lg hover:bg-slate-800">
Go home
</a>
</div>
</div>
)
}
// ─────────────────────────────────────────────────────────────────
// app/[locale]/(pages)/not-found.tsx (Server Component — no 'use client' needed)
import Link from "next/link"
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] gap-6">
<h2 className="text-6xl font-bold text-slate-100">404</h2>
<p className="text-slate-400">This page does not exist.</p>
<Link href="/" className="px-4 py-2 bg-blue-500 text-white rounded-lg">
Back to Home
</Link>
</div>
)
}
// ─────────────────────────────────────────────────────────────────
// Server Component — call notFound() for missing data
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await prisma.post.findUnique({ where: { slug: params.slug } })
if (!post) {
notFound() // ← triggers not-found.tsx, returns proper 404 status
}
return <PostContent post={post} />
}
// ─────────────────────────────────────────────────────────────────
// instrumentation.ts — central Sentry capture for ALL Server Component errors
import * as Sentry from "@sentry/nextjs"
export function onRequestError(
error: Error,
request: { path: string; method: string },
context: { routeType: string }
) {
Sentry.captureException(error, {
extra: { path: request.path, method: request.method, routeType: context.routeType }
})
}The reset prop in error.tsx calls React's error boundary reset — it attempts to re-render the error boundary's children. This works well for transient errors (network flicker, temporary API unavailability) but won't fix a persistent error from bad data or a bug. A good error.tsx UI: a clear error message, a 'Try again' button that calls reset(), and a 'Go home' link that navigates away. For Server Component errors, reset() may trigger a server re-render — if the underlying data issue is resolved, the component renders successfully. If not, the error boundary catches it again.
error.tsx runs client-side and can't access your server-side logging (Pino, Winston, Sentry server integration). Client-side errors in error.tsx should be captured with a client-side error reporting service (Sentry's captureException, Axiom's browser SDK). But server-side errors that caused the boundary to activate won't automatically be sent to Sentry's server integration — you need to use Sentry's captureServerComponentException in your Server Components' try/catch blocks, or configure Next.js's onRequestError instrumentation hook (Next.js 15) to capture all unhandled Server Component errors centrally.
not-found.tsx renders when notFound() is called from a Server Component or when no route matches the URL. Call notFound() in your data fetching logic: if a blog post or product with the requested slug doesn't exist in the database, call notFound() rather than returning null and rendering a broken page. This gives users a proper 404 experience with navigation options. Customize not-found.tsx with your site's navigation so users can find other content — a blank 404 with no navigation is a dead end.
My standard Next.js error handling setup: error.tsx at the (pages) route group level with a styled error card, reset button, and home link. not-found.tsx at the root with search suggestions and popular pages. global-error.tsx as a minimal bare-bones fallback (just a button to refresh). Sentry for error capture — the onRequestError hook in instrumentation.ts captures all Server Component errors, and Sentry.captureException in error.tsx captures client-side errors. This setup catches everything and gives me a Slack notification within seconds of a production error.
Error boundaries are only as good as your testing. Test them: (1) Throw deliberately in a Server Component and verify error.tsx renders. (2) Call notFound() and verify not-found.tsx renders. (3) Throw in your root layout and verify global-error.tsx renders. (4) Verify that error.message is NOT shown in production (server-side error details should be sanitized). (5) Verify Sentry receives the error with proper context. Next.js's dev mode shows full error details; switch to production build for testing the sanitized experience. Untested error boundaries give you a false sense of security.