Next.js Error Handling

Implement custom error pages, error boundaries, and global error handling strategies.

TL;DR

  1. 01Create error.tsx files for segment-level error boundaries.
  2. 02Create not-found.tsx for 404 pages.
  3. 03Handle errors in server actions and API routes explicitly.

Tips

  1. 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

  1. 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 useActionState to wire server action errors directly into a form — the action receives prevState as 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.tsx to 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.tsx renders 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 digest to 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 useEffect of any error boundary component.

    useEffect(() => {
      Sentry.captureException(error);
      console.error("digest:", error.digest);
    }, [error]);
    
  • Nest error.tsx files 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