Error Handling
Recipe
Handle errors gracefully in Next.js 15+ App Router using error.tsx boundaries, global-error.tsx for root-level failures, not-found.tsx for 404s, and structured error logging.
Working Example
Route-Level Error Boundary
// app/dashboard/error.tsx
"use client";
import { useEffect } from "react";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log to your error reporting service
console.error("Dashboard error:", error);
}, [error]);
return (
<div role="alert">
<h2>Something went wrong</h2>
<p>{error.message}</p>
{error.digest && (
<p className="text-sm text-gray-500">Error ID: {error.digest}</p>
)}
<button onClick={reset}>Try again</button>
</div>
);
}Global Error Handler
// app/global-error.tsx
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<div role="alert">
<h1>Application Error</h1>
<p>An unexpected error occurred.</p>
<button onClick={reset}>Reload</button>
</div>
</body>
</html>
);
}Not Found Page
// app/not-found.tsx
import Link from "next/link";
export default function NotFound() {
return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<Link href="/">Go home</Link>
</div>
);
}Programmatic Not Found in Server Components
// app/posts/[slug]/page.tsx
import { notFound } from "next/navigation";
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await db.post.findUnique({ where: { slug } });
if (!post) {
notFound(); // Renders the nearest not-found.tsx
}
return <article>{post.content}</article>;
}Error Logging Utility
// lib/logger.ts
type ErrorContext = {
userId?: string;
path?: string;
action?: string;
metadata?: Record<string, unknown>;
};
export function logError(error: unknown, context?: ErrorContext) {
const errorObj = error instanceof Error ? error : new Error(String(error));
const payload = {
message: errorObj.message,
stack: errorObj.stack,
timestamp: new Date().toISOString(),
...context,
};
// Replace with Sentry, Axiom, or your preferred service
if (process.env.NODE_ENV === "production") {
fetch("/api/log", {
method: "POST",
body: JSON.stringify(payload),
}).catch(() => {
// Swallow logging errors to prevent cascading failures
});
} else {
console.error("[Error]", payload);
}
}Server Action Error Handling
// app/actions.ts
"use server";
import { logError } from "@/lib/logger";
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string };
export async function createPost(
formData: FormData
): Promise<ActionResult<{ id: string }>> {
try {
const title = formData.get("title") as string;
if (!title) {
return { success: false, error: "Title is required" };
}
const post = await db.post.create({ data: { title } });
return { success: true, data: { id: post.id } };
} catch (error) {
logError(error, { action: "createPost" });
return { success: false, error: "Failed to create post" };
}
}Deep Dive
How It Works
error.tsxis a Client Component that wraps the route segment's children in a React Error Boundary. It catches errors thrown during rendering, in Server Components, and during data fetching.global-error.tsxcatches errors in the root layout. It must render its own<html>and<body>tags because it replaces the entire root layout when triggered.not-found.tsxis rendered whennotFound()is called or when no route matches. The nearestnot-found.tsxin the component tree is used.- Error boundaries do not catch errors in event handlers,
useEffectcleanup, or async code in Client Components. Use try/catch for those. - The
digestproperty is a hash generated by Next.js for server-side errors. It allows correlating user-facing errors with server logs without exposing sensitive stack traces. - The
resetfunction attempts to re-render the error boundary's children. It works for transient errors (network issues) but not for persistent bugs.
Variations
Error Boundary with Retry and Fallback:
"use client";
import { useEffect, useState } from "react";
export default function ErrorWithRetry({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
if (retryCount > 0) {
reset();
}
}, [retryCount, reset]);
if (retryCount >= 3) {
return (
<div>
<h2>Persistent Error</h2>
<p>Please contact support. Error ID: {error.digest}</p>
</div>
);
}
return (
<div role="alert">
<h2>Error Occurred</h2>
<button onClick={() => setRetryCount((c) => c + 1)}>
Retry ({3 - retryCount} attempts remaining)
</button>
</div>
);
}Route Handler Error Handling:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
import { logError } from "@/lib/logger";
export async function GET(request: NextRequest) {
try {
const posts = await db.post.findMany();
return NextResponse.json(posts);
} catch (error) {
logError(error, { path: "/api/posts", action: "GET" });
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}TypeScript Notes
- The
errorprop type isError & { digest?: string }. Thedigestis optional and only present for server-side errors. - Server Actions should return a discriminated union (
{ success: true; data: T } | { success: false; error: string }) for type-safe error handling on the client. - Avoid using
throwin Server Actions for validation errors. Reservethrowfor unexpected failures that should trigger the error boundary.
Gotchas
error.tsxmust be a Client Component. It requires the"use client"directive. Forgetting this produces a build error.error.tsxdoes not catch errors in the same-levellayout.tsx. To catch layout errors, placeerror.tsxin the parent segment, or useglobal-error.tsxfor the root layout.global-error.tsxonly activates in production. In development, the Next.js error overlay is shown instead.redirect()throws a special error. If you wrapredirect()in a try/catch inside a Server Component, the redirect will be caught and swallowed. Either rethrowNEXT_REDIRECTerrors or callredirect()outside try/catch.- Server-side error details are hidden in production. Next.js strips stack traces and messages from errors shown to the client. Use the
digestto correlate with server logs.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
error.tsx boundary | Built-in, automatic, per-route | Client Component only, no layout errors |
global-error.tsx | Catches root layout errors | Must render own html/body, production only |
| Try/catch in Server Actions | Granular control, return typed errors | Manual, no automatic boundary |
| Sentry or Datadog | Rich error tracking, alerts | External dependency, cost |
React ErrorBoundary class | Full control, reusable | Verbose, no Server Component errors |
FAQs
Why must error.tsx be a Client Component?
- React Error Boundaries are a client-side concept that rely on class component lifecycle methods.
- The
"use client"directive is required forerror.tsxto function as an error boundary. - Forgetting the directive produces a build error.
What is the digest property on the error object?
- It is a hash generated by Next.js for server-side errors.
- It allows you to correlate user-facing errors with server logs without exposing stack traces.
- It is only present on errors that originated on the server.
Can error.tsx catch errors thrown in the same-level layout.tsx?
- No.
error.tsxonly catches errors in the route segment's children. - To catch layout errors, place
error.tsxin the parent segment. - For root layout errors, use
global-error.tsx.
Why does global-error.tsx need its own <html> and <body> tags?
- When
global-error.tsxactivates, it replaces the entire root layout. - Without its own
<html>and<body>, the page would have no document structure. - This only activates in production; development shows the Next.js error overlay.
Gotcha: What happens if you wrap redirect() in a try/catch inside a Server Component?
redirect()throws a specialNEXT_REDIRECTerror.- A try/catch will catch and swallow this error, preventing the redirect.
- Either rethrow
NEXT_REDIRECTerrors or callredirect()outside the try/catch block.
When should you use throw vs. returning an error object in a Server Action?
- Use
throwfor unexpected failures that should trigger the nearest error boundary. - Return a typed error object (
{ success: false; error: string }) for expected validation errors. - This gives the client fine-grained control over how to display validation messages.
What does the reset function do, and when does it fail?
reset()re-renders the error boundary's children, attempting recovery.- It works for transient errors like network timeouts.
- It does not fix persistent bugs; the same error will be thrown again on re-render.
How would you type a Server Action result as a discriminated union in TypeScript?
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string };
export async function createPost(
formData: FormData
): Promise<ActionResult<{ id: string }>> {
// ...
}- The discriminant
successlets TypeScript narrow the type when checking the result.
How do you type the error prop in error.tsx with TypeScript?
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
// ...
}Error & { digest?: string }is the required type. Thedigestis optional.
Why does the error logging utility swallow its own fetch errors?
- If the logging service itself is down, a thrown error would cascade and mask the original error.
- Using
.catch(() => {})ensures logging failures are silent. - This prevents a broken logger from taking down the entire application.
Gotcha: Why are server-side error details hidden from the client in production?
- Next.js strips stack traces and error messages to prevent leaking sensitive implementation details.
- Only the
digesthash is exposed, which you can use to find the full error in server logs. - In development, the full error overlay is shown instead.
What errors does error.tsx NOT catch?
- Errors in event handlers and
useEffectcleanup functions. - Errors in async code within Client Components (unless caught manually).
- Errors in the same-level
layout.tsx. - Use try/catch for event handlers and
global-error.tsxfor root layout failures.
Related
- Authentication - handling auth failures
- API Route Handlers - error responses in Route Handlers
- Testing - testing error states
- Next.js Error Handling Docs