Route-level Error Boundaries
An error.tsx file must be a Client Component (it receives the error and reset props). It catches errors thrown inside its sibling page.tsx or any nested layouts.
Use global-error.tsx to catch errors inside the root layout itself.
Error Handling with error.tsx
No matter how carefully you write your code, things go wrong in production — APIs go down, database queries fail, unexpected data shapes cause crashes. The question isn't whether errors will happen, it's whether your app handles them gracefully or shows users a broken white screen.
The App Router gives you a file-based error handling system that mirrors how
loading.tsx works. Drop an error.tsx file into any route folder
and Next.js automatically catches runtime errors for that route, shows a friendly fallback
UI, and gives the user a way to recover — all without a full page reload.
1. Creating Your First error.tsx
Create a file named error.tsx in any route folder. There are two hard rules
for this file:
- It must be a Client Component — add
"use client"at the top. - It must accept two props:
error(the Error object) andreset(a function to retry).
// app/dashboard/error.tsx
"use client";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="flex flex-col items-center justify-center h-64 gap-4">
<h2 className="text-xl font-semibold text-red-600">Something went wrong</h2>
<p className="text-gray-500">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Try again
</button>
</div>
);
}
That's it. Now if anything inside app/dashboard/page.tsx — or any nested
route under /dashboard — throws an error, this component renders instead
of the broken page.
2. How error.tsx Works Under the Hood
Just like loading.tsx, Next.js wraps your page in a React Error Boundary
automatically when it sees an error.tsx file:
// What Next.js generates internally:
<ErrorBoundary fallback={<DashboardError />}>
<DashboardPage />
</ErrorBoundary>
React Error Boundaries only work in Client Components — that's why error.tsx
must be a Client Component even though the page it's protecting is a Server Component.
The error boundary lives on the client; it catches errors that bubble up from the server
rendering process.
The digest Property
Notice the type includes digest?: string. This is a hash that Next.js
attaches to server-side errors. The actual error message from the server is
not sent to the client in production (to avoid leaking sensitive info).
Instead, Next.js logs the full error server-side and gives you the digest
to correlate client errors with server logs.
// In development: error.message shows the real error
// In production: error.message is generic — use error.digest for log correlation
console.error("Error digest:", error.digest);
3. The reset Function — Letting Users Recover
The reset prop is a function that attempts to re-render the error boundary's
contents. When the user clicks "Try again", React unmounts the error UI and tries to
render the original page component again.
This is powerful because the user doesn't lose their place in the app — layouts stay mounted, navigation history is intact, and if the underlying issue was transient (a brief API outage, a network blip), the retry succeeds without a full reload.
"use client";
import { useEffect } from "react";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
// Log the error to an error reporting service
useEffect(() => {
console.error(error);
// e.g. Sentry.captureException(error);
}, [error]);
return (
<div className="rounded-lg border border-red-200 bg-red-50 p-6">
<h2 className="text-lg font-semibold text-red-800 mb-2">
Failed to load dashboard
</h2>
<p className="text-red-600 text-sm mb-4">
This could be a temporary issue. Please try again.
</p>
<div className="flex gap-3">
<button
onClick={reset}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 text-sm"
>
Try again
</button>
<a
href="/dashboard"
className="px-4 py-2 border border-red-300 text-red-700 rounded hover:bg-red-100 text-sm"
>
Reload page
</a>
</div>
</div>
);
}
Notice the useEffect — because error.tsx is a Client Component,
you can use hooks. This is a great place to log errors to a service like Sentry or
Datadog before showing the user the fallback UI.
4. Nested Error Boundaries
Error boundaries nest just like layouts and loading files. An error.tsx
only catches errors from its sibling page.tsx and nested routes — not
from parent layouts.
app/
├── error.tsx ← Catches errors on the root page only
├── page.tsx
└── dashboard/
├── error.tsx ← Catches errors in /dashboard and children
├── page.tsx
└── settings/
├── error.tsx ← Catches errors in /dashboard/settings only
└── page.tsx
When an error occurs in /dashboard/settings/page.tsx, Next.js looks for
the nearest error.tsx — which is dashboard/settings/error.tsx.
If that file doesn't exist, it walks up to dashboard/error.tsx. If that
doesn't exist either, it walks up to the root error.tsx.
This means you can have granular error handling — a settings page error shows a small inline error, but a dashboard-wide error shows a bigger fallback covering the whole main area.
5. Errors in Layouts — global-error.tsx
Here's an important limitation: error.tsx does not catch errors
thrown inside its sibling layout.tsx. Think about why — the
error boundary wraps the page content, but it sits inside the layout. If the layout
itself crashes, the error boundary never mounts.
// error.tsx does NOT catch errors thrown here:
// app/dashboard/layout.tsx ← layout crashes = error.tsx never renders
For errors in the root layout specifically, Next.js provides a special file:
global-error.tsx at the root of app/:
// app/global-error.tsx
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
// global-error MUST include html and body tags
// because it replaces the root layout when that layout crashes
<html>
<body>
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
<h1 className="text-2xl font-bold">Something went seriously wrong</h1>
<button onClick={reset} className="px-4 py-2 bg-black text-white rounded">
Try again
</button>
</div>
</body>
</html>
);
}
global-error.tsx is the last line of defence — it only activates when the
root layout itself crashes. Because it completely replaces the root layout, it must
include its own <html> and <body> tags.
In practice, errors in the root layout are rare. But having global-error.tsx
means even a catastrophic crash shows a recoverable UI instead of a blank white page.
6. notFound() — A Special Case of Error Handling
A 404 — "this resource doesn't exist" — is technically an error, but it's different
enough from a crash that Next.js handles it separately. Call notFound()
from next/navigation to trigger a 404 response:
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) {
notFound(); // Triggers the not-found.tsx file for this route
}
return <article>{post.title}</article>;
}
Create a not-found.tsx file to customise what the user sees:
// app/blog/[slug]/not-found.tsx
import Link from "next/link";
export default function PostNotFound() {
return (
<div className="flex flex-col items-center justify-center h-64 gap-4">
<h2 className="text-2xl font-bold">Post Not Found</h2>
<p className="text-gray-500">
The post you're looking for doesn't exist or has been removed.
</p>
<Link href="/blog" className="text-blue-600 hover:underline">
Back to Blog
</Link>
</div>
);
}
Unlike error.tsx, not-found.tsx is a Server Component by
default — you don't need "use client" unless you add interactivity.
It also doesn't receive error or reset props.
A root app/not-found.tsx serves as the global 404 page for any URL that
doesn't match a route in your app.
7. Throwing Errors Intentionally
You can throw errors deliberately in Server Components to trigger the nearest
error.tsx. This is useful for enforcing preconditions or handling
unexpected states:
// app/dashboard/page.tsx
export default async function DashboardPage() {
const data = await fetchDashboardData();
if (!data) {
throw new Error("Failed to load dashboard data");
// Nearest error.tsx renders with this error object
}
return <Dashboard data={data} />;
}
A few things to keep in mind when throwing intentionally:
- In development, the full error message and stack trace show in the browser overlay so you can debug easily.
- In production, the error message is hidden from the client for security — only the
digesthash is exposed. Log the full error server-side. - Don't throw errors for expected conditions like "user not found" — use
notFound()for those. Save throws for genuinely unexpected failures.
8. Error Handling in Server Actions
When a Server Action throws an error, it doesn't automatically trigger error.tsx
— because Server Actions are called from Client Components, the error is returned to the
client and you handle it there. The standard pattern is using a try/catch:
// app/dashboard/_components/CreatePostForm.tsx
"use client";
import { useState } from "react";
import { createPost } from "@/app/actions";
export default function CreatePostForm() {
const [error, setError] = useState<string | null>(null);
async function handleSubmit(formData: FormData) {
try {
await createPost(formData);
setError(null);
} catch (e) {
setError("Failed to create post. Please try again.");
}
}
return (
<form action={handleSubmit}>
{error && (
<p className="text-red-600 text-sm mb-4">{error}</p>
)}
<input name="title" placeholder="Post title" />
<button type="submit">Create</button>
</form>
);
}
We'll cover Server Actions in full detail in Module 2. The key point here is that
action errors are handled inline in the form — they don't bubble up to error.tsx.
9. A Complete Error Handling Strategy
Here's a summary of which tool to use for each situation:
| Situation | Tool |
|---|---|
| Runtime crash in a page or nested component | error.tsx |
| Runtime crash in the root layout | global-error.tsx |
| Resource doesn't exist (404) | notFound() + not-found.tsx |
| Error inside a Server Action form | try/catch + inline error state |
| Redirect unauthenticated users | redirect() in Server Component |
10. Common Gotchas
-
Forgetting
"use client"on error.tsx. Next.js will throw a build error iferror.tsxisn't a Client Component. It's the one file in the App Router that is required to be a Client Component. -
Expecting error.tsx to catch layout errors. It won't — use
global-error.tsxfor the root layout and keep nested layouts simple and fast to avoid this problem. -
Showing raw error messages in production. Don't render
{error.message}directly in your error UI for production — it may be a generic "Internal Server Error" that confuses users. Write a friendly, human-readable message instead and useerror.digestfor debugging. -
Not logging errors. The
useEffect(() => { logError(error) }, [error])pattern insideerror.tsxis easy to skip but important — without it you'll have no visibility into what's going wrong in production. -
Confusing error.tsx and not-found.tsx.
error.tsxis for unexpected crashes.not-found.tsxis for expected "this doesn't exist" scenarios. Use the right tool — a 404 shown viaerror.tsxwon't send the correct HTTP 404 status code to search engines.
Key Takeaways
error.tsxmust be a Client Component and receiveserrorandresetprops.- It catches runtime errors from its sibling
page.tsxand all nested routes beneath it. - The
reset()function lets users retry without a full page reload — layouts stay mounted. - In production, error messages are hidden from the client — log errors server-side and use
error.digestto correlate them. global-error.tsxcatches errors in the root layout and must include its own<html>and<body>tags.- Use
notFound()+not-found.tsxfor 404s — noterror.tsx. - Always log errors inside
error.tsxwithuseEffectso you have production visibility.
That wraps up Module 1 — Foundations & Routing! You now understand the App Router's core building blocks: file-based routing, layouts, navigation, loading states, and error handling. In Module 2 we go deeper into the most powerful feature of the App Router — React Server Components and data fetching.