DEV Community

Cover image for Error Handling in Next.js App Router — error.tsx, not-found.tsx, and global-error.tsx
Aon infotech
Aon infotech

Posted on

Error Handling in Next.js App Router — error.tsx, not-found.tsx, and global-error.tsx

Error handling in Next.js App Router works through a file-based convention — specific files at specific locations in your route structure automatically catch and handle errors at different levels. Understanding the hierarchy prevents the most common error handling gaps.

Here's the complete error handling system, including the patterns I use for user-facing tools like the free image generation tool for teachers where error UX directly affects how much users trust the product.


The Error Handling File Hierarchy

Next.js App Router provides four files for error handling:

app/
├── error.tsx           ← Catches errors in route segments
├── not-found.tsx       ← Handles 404s (notFound() calls)
├── global-error.tsx    ← Catches errors in root layout
└── loading.tsx         ← Loading UI (not error, but related)
Enter fullscreen mode Exit fullscreen mode

Each serves a different purpose and operates at a different level of the rendering tree.


error.tsx — The Main Error Boundary

error.tsx wraps a route segment in a React Error Boundary. When any component in that segment throws, error.tsx catches it and renders instead.

// app/dashboard/error.tsx
'use client'; // error.tsx must be a Client Component

import { useEffect } from 'react';

type ErrorProps = {
  error: Error & { digest?: string };
  reset: () => void;
};

export default function DashboardError({ error, reset }: ErrorProps) {
  useEffect(() => {
    // Log to your error reporting service
    console.error('Dashboard error:', error);
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
      <div className="text-center">
        <h2 className="text-xl font-semibold text-neutral-800">
          Something went wrong
        </h2>
        <p className="text-neutral-500 mt-1 text-sm">
          {error.digest 
            ? `Error ID: ${error.digest}` 
            : 'An unexpected error occurred'}
        </p>
      </div>
      <button
        onClick={reset}
        className="px-4 py-2 bg-orange-500 text-white rounded-full text-sm"
      >
        Try again
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Must be a Client Component ('use client')
  • Receives error object and reset function
  • reset() re-renders the segment, giving the user a retry path
  • error.digest is a hash Next.js generates for server-side errors — matches the log entry in your server logs

Nesting error.tsx Files

error.tsx only catches errors in its sibling directory and below. You can nest them:

app/
├── error.tsx           ← Catches errors in all routes
├── dashboard/
│   ├── error.tsx       ← Catches errors in /dashboard routes
│   └── settings/
│       └── error.tsx   ← Catches errors in /dashboard/settings
Enter fullscreen mode Exit fullscreen mode

The most specific error.tsx catches first. Errors bubble up if no closer handler exists.

This allows granular error UI: /dashboard/settings might show an inline error with specific settings-context help, while a top-level error shows a generic fallback.


not-found.tsx — Handling Missing Resources

not-found.tsx renders when notFound() is called from a Server Component. This is for expected 404s — resource doesn't exist, ID not found in database.

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);

  if (!post) {
    notFound(); // Triggers not-found.tsx
  }

  return <Article post={post} />;
}
Enter fullscreen mode Exit fullscreen mode
// app/blog/not-found.tsx
import Link from 'next/link';

export default function BlogNotFound() {
  return (
    <div className="flex flex-col items-center justify-center min-h-[400px] gap-4 text-center">
      <h1 className="text-2xl font-semibold">Post not found</h1>
      <p className="text-neutral-500">
        This post may have been moved or removed.
      </p>
      <Link 
        href="/blog"
        className="px-4 py-2 bg-neutral-900 text-white rounded-full text-sm"
      >
        Browse all posts
      </Link>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

not-found.tsx can be Server or Client — unlike error.tsx. It doesn't receive props.


global-error.tsx — Catching Root Layout Errors

error.tsx doesn't catch errors thrown in the root layout.tsx — the error boundary is siblings with the layout, not wrapping it. global-error.tsx handles this case.

// app/global-error.tsx
'use client';

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    // global-error replaces the root layout entirely
    // Must include html and body tags
    <html>
      <body>
        <div style={{ 
          minHeight: '100vh',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          fontFamily: 'system-ui, sans-serif'
        }}>
          <h2>Something went wrong</h2>
          <button onClick={reset}>Try again</button>
        </div>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

global-error.tsx replaces the entire page — including the root layout — so it must include <html> and <body> tags. It only fires in production (not in development, where the error overlay shows instead).


Error Reporting Integration

The error.tsx useEffect is where you connect to error reporting services:

'use client';
import { useEffect } from 'react';

export default function Error({ error, reset }: ErrorProps) {
  useEffect(() => {
    // Sentry
    Sentry.captureException(error);

    // Or any other error reporting service
    reportError({
      message: error.message,
      digest: error.digest,
      stack: error.stack,
    });
  }, [error]);

  return (/* ... */);
}
Enter fullscreen mode Exit fullscreen mode

The error.digest is critical for server-side errors: the actual error message is not sent to the client (for security), but the digest lets you match the client error to the full server-side error in your logs.


Typed Error Responses

For API routes, return typed error responses rather than throwing:

// app/api/generate/route.ts
export async function POST(request: Request) {
  try {
    const data = await processRequest(request);
    return Response.json({ success: true, data });
  } catch (error) {
    // Return structured error, don't throw
    return Response.json(
      { 
        success: false, 
        error: 'Generation failed',
        code: 'GENERATION_ERROR'
      },
      { status: 500 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Client-side, check response.ok before using data:

const response = await fetch('/api/generate', { method: 'POST', body });
if (!response.ok) {
  const error = await response.json();
  throw new Error(error.error ?? 'Request failed');
}
const data = await response.json();
Enter fullscreen mode Exit fullscreen mode

The Common Mistake

The most common error handling mistake: not providing a reset path. An error component that just shows "something went wrong" with no way forward forces users to refresh the entire page, which loses their context and feels like the app broke.

Always provide: a clear error message, what to try, and a reset() button that retries the failed segment. Where appropriate, also provide navigation away (home page link, search) in case retry fails too.


Testing Error Boundaries

Testing that error boundaries actually catch errors requires deliberately throwing:

// components/ErrorThrower.tsx (test utility)
'use client';
import { useState } from 'react';

export function ErrorThrower() {
  const [shouldThrow, setShouldThrow] = useState(false);

  if (shouldThrow) {
    throw new Error('Test error from ErrorThrower');
  }

  return (
    <button onClick={() => setShouldThrow(true)}>
      Trigger test error
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Place this in a route surrounded by your error.tsx to verify the error boundary renders correctly. Remove before production.

For automated testing of error states, React Testing Library's console.error mocking prevents test pollution from expected error outputs:

test('error boundary shows error UI', () => {
  const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

  render(<ComponentThatThrows />);

  expect(screen.getByText('Something went wrong')).toBeInTheDocument();
  expect(screen.getByRole('button', { name: 'Try again' })).toBeInTheDocument();

  consoleSpy.mockRestore();
});
Enter fullscreen mode Exit fullscreen mode

Summary

The App Router error handling hierarchy: error.tsx for route-segment errors, not-found.tsx for missing resources via notFound(), global-error.tsx for root layout errors. Nest error.tsx at the granularity you need — more specific error UI for more specific routes.

Always include a reset() retry path. Always log errors in useEffect. Use error.digest to correlate client errors with server logs.


Quick Reference

File Catches Must be client? Receives props?
error.tsx Segment errors Yes error, reset
not-found.tsx notFound() calls No None
global-error.tsx Root layout errors Yes error, reset

Place each file as a sibling to the page.tsx you want it to protect. global-error.tsx goes directly in the app/ directory.

Top comments (0)