ProgrUmar Logo
Module 1: Foundations & Routing

Error Handling with error.tsx

Duration: 9 mins

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) and reset (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 digest hash 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 if error.tsx isn'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.tsx for 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 use error.digest for debugging.
  • Not logging errors. The useEffect(() => { logError(error) }, [error]) pattern inside error.tsx is 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.tsx is for unexpected crashes. not-found.tsx is for expected "this doesn't exist" scenarios. Use the right tool — a 404 shown via error.tsx won't send the correct HTTP 404 status code to search engines.

Key Takeaways

  • error.tsx must be a Client Component and receives error and reset props.
  • It catches runtime errors from its sibling page.tsx and 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.digest to correlate them.
  • global-error.tsx catches errors in the root layout and must include its own <html> and <body> tags.
  • Use notFound() + not-found.tsx for 404s — not error.tsx.
  • Always log errors inside error.tsx with useEffect so 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.

Chat with us