Skip to main content
Error Handling in the Next.js App Router: What Each Error File Actually Catches

Error Handling in the Next.js App Router: What Each Error File Actually Catches

July 17, 2026
Frontend Engineering
7 min read

At Devya we ship a lot of Next.js App Router apps, and the failure that keeps recurring is a single unhandled throw taking down a whole page instead of the one panel that broke. The App Router already contains that blast radius through a small set of special files — as long as your team knows where each one sits and what it actually catches. These are our engineering notes on error.tsx, global-error.tsx, not-found.tsx, the production digest, and the reset function.

Key takeaways

An error.tsx file in a route segment creates a React error boundary that catches errors thrown while rendering that segment's page and any nested layouts, pages, and components below it.

error.tsx must start with 'use client' because React error boundaries rely on class-component lifecycle methods that only run in the browser; the file receives two props, error and reset.

error.tsx cannot catch an error thrown by the layout in its own segment, because that layout renders above the boundary — you place an error.tsx in the parent segment to catch it.

global-error.tsx is the only boundary that catches errors in the root layout; because it replaces the root layout when active, it must render its own html and body tags, and it runs only in production.

In production, Next.js strips the message from server-side errors and replaces it with a digest hash so nothing leaks to the browser; the original error is logged on the server instead.

What does error.tsx catch in the App Router?

An error.tsx file catches errors thrown while rendering the route segment it lives in and everything nested below it. When we drop it in a segment folder, Next.js automatically wraps that segment's page.tsx, its nested layouts and pages, and every Server and Client Component they render in a React error boundary. When a render throws, the boundary swaps in the error.tsx UI instead of unmounting the whole tree.

The part that trips teams up is what error.tsx does not catch: errors thrown in event handlers such as onClick, errors thrown in asynchronous code that runs after render, a notFound() call, and an error thrown by the layout.tsx or template.tsx in the same segment. An error.tsx at app/dashboard/error.tsx catches throws from app/dashboard/page.tsx and anything nested under /dashboard, but not from app/dashboard/layout.tsx.

Why must error.tsx be a Client Component?

error.tsx must be a Client Component because React error boundaries are implemented with class-component lifecycle methods — getDerivedStateFromError and componentDidCatch — that only execute in the browser. Next.js wraps the file in a boundary that needs that client runtime, so it must begin with 'use client'.

The component receives exactly two props. The first is error, an Error instance that may carry a digest string. The second is reset, a zero-argument function that re-renders the boundary. A typical error.tsx renders an alert with a heading and a button whose onClick calls reset().

When do we need global-error.tsx instead of error.tsx?

You need global-error.tsx when the error happens in the root layout or root template, which sit above every ordinary error.tsx. A regular error.tsx renders inside the layout it protects, so it can never replace a broken root layout.

global-error.tsx lives at the very top of the tree and, when it triggers, replaces the entire root layout — which is why it must include its own html and body tags. It is active only in production; in development the Next.js error overlay takes over so the team can see the real stack. Because it replaces everything, we keep its markup minimal.

How do we handle not found separately from errors?

Call notFound() from next/navigation, which throws a dedicated control-flow error caught by the nearest not-found.tsx, not by error.tsx. A missing record is an expected outcome, not a crash, so it deserves its own boundary and a real HTTP 404 status.

Calling notFound() inside a segment renders that segment's not-found.tsx and returns a 404 on the initial load; the root app/not-found.tsx also handles URLs that match no route. In practice we call notFound() right after a data fetch returns nothing, before rendering the component that assumes the record exists.

Why are server error messages replaced with a digest in production?

Next.js deliberately strips the message and stack from errors thrown on the server in production and replaces them with a digest property — an auto-generated hash — so it never leaks internals or secrets to the browser. The full error is logged on the server, and the digest is what lets you match a user's report to that server log.

In development you still see the real message and stack. The practical consequence is that inside a production error.tsx, error.message for a server error is generic, so we build our correlation and reporting around error.digest.

How do we recover without a reload, and what about event-handler errors?

Call the reset() prop to recover without a full page reload; it asks React to re-render the boundary's contents by re-attempting the segment that failed. If the cause was transient — a flaky fetch that now succeeds — the segment comes back with no navigation.

For the errors error.tsx never sees, namely those thrown in event handlers and async callbacks, we catch them in a try/catch and drive the UI with state, rendering an inline alert. And for expected failures in a Server Action, such as a validation error or a duplicate email, we return a typed error value rather than throwing, so the form renders the message inline without ever tripping an error boundary.

The App Router error files at a glance

error.tsx catches render errors in its segment and nested children, must be a Client Component, does not catch its own layout, and receives error plus reset.

global-error.tsx catches errors in the root layout or template, must be a Client Component, replaces the root layout so it must render html and body, and runs in production only.

not-found.tsx catches notFound() calls and unmatched routes, does not need to be a Client Component, returns HTTP 404, and is not triggered by real crashes.

Errors thrown in event handlers or async callbacks are caught by none of these files; handle them with try/catch and surface them with state.

FAQ

Q: Does error.tsx catch errors in its own layout.tsx?

A: No. The layout renders above the boundary that error.tsx defines. To catch an error thrown by a segment's layout, put an error.tsx in the parent segment.

Q: Why does my error.tsx not appear in development?

A: Next.js shows its development error overlay on top of your boundary, and global-error.tsx only takes over in a production build. Run a production build to see your real error UI.

Q: Do error boundaries catch errors in onClick handlers?

A: No. React error boundaries only catch errors thrown during rendering. Wrap event-handler and async logic in try/catch and surface the failure with component state.

Q: What is the digest property on the error object?

A: A hash Next.js generates for server-side errors in production, replacing the real message so nothing leaks to the client. Match it against your server logs to find the underlying error.

Q: Does calling notFound() trigger error.tsx?

A: No. notFound() throws a dedicated control-flow signal caught by the nearest not-found.tsx and returns a 404. error.tsx is only for unexpected exceptions.