React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

errorserror-boundaryglobal-errorloggingnot-founderror-tsx

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.tsx is 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.tsx catches 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.tsx is rendered when notFound() is called or when no route matches. The nearest not-found.tsx in the component tree is used.
  • Error boundaries do not catch errors in event handlers, useEffect cleanup, or async code in Client Components. Use try/catch for those.
  • The digest property 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 reset function 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 error prop type is Error & { digest?: string }. The digest is 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 throw in Server Actions for validation errors. Reserve throw for unexpected failures that should trigger the error boundary.

Gotchas

  1. error.tsx must be a Client Component. It requires the "use client" directive. Forgetting this produces a build error.
  2. error.tsx does not catch errors in the same-level layout.tsx. To catch layout errors, place error.tsx in the parent segment, or use global-error.tsx for the root layout.
  3. global-error.tsx only activates in production. In development, the Next.js error overlay is shown instead.
  4. redirect() throws a special error. If you wrap redirect() in a try/catch inside a Server Component, the redirect will be caught and swallowed. Either rethrow NEXT_REDIRECT errors or call redirect() outside try/catch.
  5. Server-side error details are hidden in production. Next.js strips stack traces and messages from errors shown to the client. Use the digest to correlate with server logs.

Alternatives

ApproachProsCons
error.tsx boundaryBuilt-in, automatic, per-routeClient Component only, no layout errors
global-error.tsxCatches root layout errorsMust render own html/body, production only
Try/catch in Server ActionsGranular control, return typed errorsManual, no automatic boundary
Sentry or DatadogRich error tracking, alertsExternal dependency, cost
React ErrorBoundary classFull control, reusableVerbose, 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 for error.tsx to 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.tsx only catches errors in the route segment's children.
  • To catch layout errors, place error.tsx in 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.tsx activates, 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 special NEXT_REDIRECT error.
  • A try/catch will catch and swallow this error, preventing the redirect.
  • Either rethrow NEXT_REDIRECT errors or call redirect() outside the try/catch block.
When should you use throw vs. returning an error object in a Server Action?
  • Use throw for 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 success lets 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. The digest is 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 digest hash 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 useEffect cleanup 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.tsx for root layout failures.