Next.js Error Handling
Implement custom error pages, error boundaries, and global error handling strategies.
TL;DR
- 01Create error.tsx files for segment-level error boundaries.
- 02Create not-found.tsx for 404 pages.
- 03Handle errors in server actions and API routes explicitly.
Tips
- 01Place global-error.tsx in the app root but rely on segment-level error.tsx files for most recovery UI — they give users more context-specific error messages.
Warnings
- 01Never expose sensitive error details to users — log detailed errors server-side and show friendly messages to clients.
Error Boundaries with error.tsx
- Create error.tsx for segment-level error handling.
// app/blog/error.tsx "use client"; import { useEffect } from "react"; export default function Error({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { useEffect(() => { console.error(error); }, [error]); return ( <div> <h2>Something went wrong!</h2> <button onClick={() => reset()}>Try again</button> </div> ); } - Error boundaries catch errors from child segments.
- Reset function allows users to retry.
- Must be a client component with "use client" directive.
- Nest error.tsx at each route level for targeted recovery — an error in /dashboard won't break the nav.
app/ error.tsx # catches app-level errors dashboard/ error.tsx # catches only dashboard errors settings/ error.tsx # catches only settings errors
Custom 404 Pages
- Create not-found.tsx for missing resources.
// app/blog/[slug]/not-found.tsx export default function NotFound() { return ( <div> <h1>Post not found</h1> <p>The post you're looking for doesn't exist.</p> </div> ); } - Use notFound() function to trigger the not-found page.
import { notFound } from "next/navigation"; export default async function Post({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await getPost(slug); if (!post) { notFound(); } return <article>{post.content}</article>; } - Each segment can have its own not-found.tsx.
- Add a root-level not-found.tsx for app-wide 404 handling.
// app/not-found.tsx import Link from "next/link"; export default function RootNotFound() { return ( <div> <h1>Page not found</h1> <Link href="/">Go home</Link> </div> ); } - Export metadata from not-found.tsx for SEO.
export const metadata = { title: "404 — Page not found", description: "This page does not exist." };
Server Action Error Handling
- Catch and handle errors in server actions.
"use server"; export async function createPost(formData: FormData) { try { const title = formData.get("title") as string; if (!title) { return { error: "Title is required" }; } const post = await db.post.create({ data: { title } }); return { success: true, post }; } catch (error) { return { error: "Failed to create post" }; } } - Return error objects from server actions.
- Handle errors in client components.
"use client"; export default function Form() { const [error, setError] = useState<string | null>(null); async function handleSubmit(formData: FormData) { const result = await createPost(formData); if (result.error) { setError(result.error); } } return ( <form action={handleSubmit}> {error && <p>{error}</p>} </form> ); } - Use
useActionStateto wire server action errors directly into a form — the action receivesprevStateas its first argument (React 19 / Next.js 15).// actions.ts "use server"; export async function createPost(prevState: unknown, formData: FormData) { try { await db.post.create({ data: { title: formData.get("title") } }); return { success: true }; } catch { return { error: "Failed to create post" }; } } // Form.tsx "use client"; import { useActionState } from "react"; export default function Form() { const [state, action] = useActionState(createPost, null); return ( <form action={action}> {state?.error && <p>{state.error}</p>} <button type="submit">Create</button> </form> ); } - Log the full error server-side and return only a safe message to the client — never expose stack traces or database details.
"use server"; export async function deletePost(id: string) { try { await db.post.delete({ where: { id } }); return { success: true }; } catch (error) { console.error("[deletePost] failed:", error); // full error server-side only return { error: "Could not delete post" };// safe message to client } }
API Route Error Handling
- Return error responses with appropriate status codes.
// app/api/posts/[id]/route.ts export async function GET( request: Request, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; try { const post = await getPost(id); if (!post) { return Response.json( { error: "Post not found" }, { status: 404 } ); } return Response.json(post); } catch (error) { return Response.json( { error: "Internal server error" }, { status: 500 } ); } } - Always handle errors in API routes explicitly.
- Return meaningful status codes and error messages.
- Validate request body fields and return 400 for bad input.
export async function POST(request: Request) { const body = await request.json(); if (!body.title) { return Response.json({ error: "Title is required" }, { status: 400 }); } return Response.json({ ok: true }); } - Return 401 for missing or invalid authentication tokens.
const token = request.headers.get("authorization"); if (!token || !isValid(token)) { return Response.json({ error: "Unauthorized" }, { status: 401 }); }
Global Error Handling
Create
app/global-error.tsxto catch errors in the root layout — it replaces the layout entirely, so it must render<html>and<body>tags.// app/global-error.tsx — replaces root layout; MUST include <html><body> "use client"; export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { return ( <html> <body> <h1>Application Error</h1> <button onClick={reset}>Try again</button> </body> </html> ); }Regular
error.tsxrenders inside the existing layout — never add<html>or<body>tags there.// app/dashboard/error.tsx — renders inside layout, no html/body tags "use client"; export default function Error({ reset }: { reset: () => void }) { return <div><p>Something went wrong.</p><button onClick={reset}>Retry</button></div>; }Use the error
digestto correlate client errors with server log entries.export default function Error({ error }: { error: Error & { digest?: string } }) { return <p>Error ID: {error.digest}</p>; // matches server-side error log }Add error monitoring in the
useEffectof any error boundary component.useEffect(() => { Sentry.captureException(error); console.error("digest:", error.digest); }, [error]);Nest
error.tsxfiles at each route segment for targeted recovery — a dashboard error won't break the nav or sidebar.app/ error.tsx ← catches app-level errors (inside root layout) global-error.tsx ← catches root layout errors (replaces layout) dashboard/ error.tsx ← catches only dashboard errors
FAQ
error.tsx creates a React error boundary that catches rendering errors in a route segment and its children, while try/catch handles runtime errors in async server components or server actions. Use both: error.tsx for UI recovery, try/catch for data-fetching logic.
Place error.tsx files at each route segment level where you want isolated error recovery — for example, app/dashboard/error.tsx catches errors only in the dashboard segment without breaking the rest of the layout. Nesting them gives users more targeted recovery options.
Return a Response with an appropriate HTTP status code and a JSON body: return Response.json({ error: 'Not found' }, { status: 404 }). Always use 4xx for client errors and 5xx for server errors so consuming clients can handle them correctly.
Wrap your server action logic in a try/catch and return a structured result object like { success: false, error: 'message' } rather than rethrowing, since uncaught server action errors surface as generic failures to the client. Use the error only to inform the UI, never to expose stack traces or DB details.
Add a not-found.tsx file in the app directory (or a specific route segment) and call the notFound() function from 'next/navigation' anywhere in a server component to trigger it. This replaces the default Next.js 404 page for that segment.