Error Boundaries in the Next.js App Router
How error.tsx, global-error.tsx, and not-found.tsx work in the Next.js App Router — when each one fires, how segments isolate failures, and patterns for recovery and observability.
What you'll learn
- ✓How error.tsx scopes errors to a route segment
- ✓The difference between error.tsx and global-error.tsx
- ✓How to trigger and customize not-found.tsx
- ✓Patterns for logging and recovery
Prerequisites
- •Basic familiarity with the App Router file conventions
What and Why
In a single-page app, an uncaught error can blank the entire screen. The Next.js App Router fixes this with file conventions — drop an error.tsx next to a page.tsx and that segment becomes its own React error boundary. Failures in a sidebar do not take down the header; a broken nested route shows a friendly retry button while the layout above it stays mounted.
This matters because real apps have flaky dependencies. A search API hiccups, a recommendation service times out, a third-party widget throws. Without per-segment boundaries, those failures cascade. With them, the blast radius is tiny.
Mental Model
Each folder in app/ can hold up to three error-handling files. error.tsx catches render errors in that segment and its children, excluding the layout of that same folder. global-error.tsx (only at the root of app/) catches errors in the root layout itself — the one place error.tsx cannot reach. not-found.tsx renders when you call notFound() from a server component or when a dynamic segment fails to match.
Error files are always Client Components because they need to render reset buttons and receive an error prop plus a reset() function from React.
Hands-on Example
Suppose /dashboard shows a sidebar plus a feed. The feed sometimes fails. Create:
// app/dashboard/feed/error.tsx
'use client';
import { useEffect } from 'react';
export default function FeedError({ error, reset }) {
useEffect(() => {
console.error('[feed]', error);
}, [error]);
return (
<div className="p-4 border rounded">
<p>Couldn't load the feed.</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
Now when app/dashboard/feed/page.tsx throws, the dashboard layout and sidebar keep rendering, and only the feed area shows the recovery UI. Click “Try again” and React re-renders the segment.
app/
layout.tsx <- only global-error.tsx catches here
global-error.tsx <- catches root layout errors (renders own <html>)
error.tsx <- catches errors in pages directly under app/
dashboard/
layout.tsx <- caught by app/error.tsx (parent)
error.tsx <- catches dashboard children's errors
feed/
page.tsx <- throws
error.tsx <- THIS catches it; sidebar stays mounted
not-found.tsx <- renders when feed calls notFound() For the global case, create app/global-error.tsx. Because it replaces the root layout when triggered, it must render its own <html> and <body>:
'use client';
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>Something went very wrong.</h2>
<button onClick={reset}>Reload</button>
</body>
</html>
);
}
For 404s, throw notFound() from a server component when a record is missing:
import { notFound } from 'next/navigation';
const post = await db.post.find(id);
if (!post) notFound();
Then app/posts/[id]/not-found.tsx renders the friendly message.
Common Pitfalls
The first surprise is that error.tsx does not catch errors in the layout of its own folder. If app/dashboard/layout.tsx throws, app/dashboard/error.tsx cannot help — the error bubbles up to the parent boundary. Put critical work outside layouts when you need fine-grained recovery.
Second, errors in event handlers are not caught. Error boundaries only catch render-phase errors. A failing onClick needs a try/catch and explicit state. Many teams forget this and wonder why their boundary stays silent.
Third, global-error.tsx only runs in production builds. In development, Next.js shows its own overlay so you can debug. Test global error UI with next build && next start.
Fourth, forgetting to log. The error prop has a digest field on the server side — a hash that matches your server logs. Always send it to your observability tool so the friendly UI maps back to a real stack trace.
Best Practices
Place an error.tsx at every meaningful segment, not just the root. Keep the recovery UI small, accessible, and honest about what failed. Always wire the useEffect log to your tracker (Sentry, Datadog, etc.) with the digest value. Use not-found.tsx for missing resources rather than rendering empty states inline — it gives you proper status codes and a clean URL story. For experimental features, wrap them in a nested route with its own boundary so a bad release degrades gracefully.
Wrap-up
error.tsx, global-error.tsx, and not-found.tsx give you React error boundaries with a route-aware scope. Used together, they turn cascading failures into isolated, recoverable UI — small files that pay off the first time production gets weird.
Related articles
- Next.js Parallel and Intercepting Routes in Next.js
How parallel slots and intercepting routes power dashboards, modals, and tabbed UIs in the Next.js App Router — the file conventions, when to reach for each, and the patterns that hold up in production.
- Web Next.js App Router vs Pages Router
Compare the Next.js App Router and Pages Router: routing, data fetching, layouts, server components, and how to decide for new and existing projects.
- Next.js Deploying Next.js on Vercel: A Practical Guide
A hands-on walkthrough for shipping a Next.js app to Vercel — connecting Git, configuring environment variables, understanding preview deployments, and avoiding the usual production gotchas.
- Next.js Next.js Dynamic Routes and Catch-All Segments Explained
Master dynamic routes, catch-all segments, and optional catch-alls in Next.js. Learn how the file-based router maps URLs to components with concrete examples.