Skip to content
C Codeloom
Next.js

Error Boundaries in the Next.js App Router

How error.tsx, global-error.tsx, and not-found.tsx work in the Next.js App Router — when each one fires, how segments isolate failures, and patterns for recovery and observability.

·4 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How error.tsx scopes errors to a route segment
  • The difference between error.tsx and global-error.tsx
  • How to trigger and customize not-found.tsx
  • Patterns for logging and recovery

Prerequisites

  • Basic familiarity with the App Router file conventions

What and Why

In a single-page app, an uncaught error can blank the entire screen. The Next.js App Router fixes this with file conventions — drop an error.tsx next to a page.tsx and that segment becomes its own React error boundary. Failures in a sidebar do not take down the header; a broken nested route shows a friendly retry button while the layout above it stays mounted.

This matters because real apps have flaky dependencies. A search API hiccups, a recommendation service times out, a third-party widget throws. Without per-segment boundaries, those failures cascade. With them, the blast radius is tiny.

Mental Model

Each folder in app/ can hold up to three error-handling files. error.tsx catches render errors in that segment and its children, excluding the layout of that same folder. global-error.tsx (only at the root of app/) catches errors in the root layout itself — the one place error.tsx cannot reach. not-found.tsx renders when you call notFound() from a server component or when a dynamic segment fails to match.

Error files are always Client Components because they need to render reset buttons and receive an error prop plus a reset() function from React.

Hands-on Example

Suppose /dashboard shows a sidebar plus a feed. The feed sometimes fails. Create:

// app/dashboard/feed/error.tsx
'use client';
import { useEffect } from 'react';

export default function FeedError({ error, reset }) {
  useEffect(() => {
    console.error('[feed]', error);
  }, [error]);
  return (
    <div className="p-4 border rounded">
      <p>Couldn't load the feed.</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Now when app/dashboard/feed/page.tsx throws, the dashboard layout and sidebar keep rendering, and only the feed area shows the recovery UI. Click “Try again” and React re-renders the segment.

app/
layout.tsx          <- only global-error.tsx catches here
global-error.tsx    <- catches root layout errors (renders own <html>)
error.tsx           <- catches errors in pages directly under app/
dashboard/
  layout.tsx        <- caught by app/error.tsx (parent)
  error.tsx         <- catches dashboard children's errors
  feed/
    page.tsx        <- throws
    error.tsx       <- THIS catches it; sidebar stays mounted
    not-found.tsx   <- renders when feed calls notFound()
Where each error file catches

For the global case, create app/global-error.tsx. Because it replaces the root layout when triggered, it must render its own <html> and <body>:

'use client';
export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <h2>Something went very wrong.</h2>
        <button onClick={reset}>Reload</button>
      </body>
    </html>
  );
}

For 404s, throw notFound() from a server component when a record is missing:

import { notFound } from 'next/navigation';
const post = await db.post.find(id);
if (!post) notFound();

Then app/posts/[id]/not-found.tsx renders the friendly message.

Common Pitfalls

The first surprise is that error.tsx does not catch errors in the layout of its own folder. If app/dashboard/layout.tsx throws, app/dashboard/error.tsx cannot help — the error bubbles up to the parent boundary. Put critical work outside layouts when you need fine-grained recovery.

Second, errors in event handlers are not caught. Error boundaries only catch render-phase errors. A failing onClick needs a try/catch and explicit state. Many teams forget this and wonder why their boundary stays silent.

Third, global-error.tsx only runs in production builds. In development, Next.js shows its own overlay so you can debug. Test global error UI with next build && next start.

Fourth, forgetting to log. The error prop has a digest field on the server side — a hash that matches your server logs. Always send it to your observability tool so the friendly UI maps back to a real stack trace.

Best Practices

Place an error.tsx at every meaningful segment, not just the root. Keep the recovery UI small, accessible, and honest about what failed. Always wire the useEffect log to your tracker (Sentry, Datadog, etc.) with the digest value. Use not-found.tsx for missing resources rather than rendering empty states inline — it gives you proper status codes and a clean URL story. For experimental features, wrap them in a nested route with its own boundary so a bad release degrades gracefully.

Wrap-up

error.tsx, global-error.tsx, and not-found.tsx give you React error boundaries with a route-aware scope. Used together, they turn cascading failures into isolated, recoverable UI — small files that pay off the first time production gets weird.