Type something to search...
Next.js error.js

Next.js error.js

An error.js file gives a route segment its own React error boundary, automatically. Instead of manually wrapping trees of components in error-catching logic the way you would in a plain React app, Next.js recognizes this one special filename and wires up the boundary for you — at exactly the right place in the component tree, scoped to exactly the segment (and its children) that the file lives next to.

This article covers the file's actual contract in detail: the two props it receives, what it can and can't catch, how it nests relative to the rest of the special files in a route segment, and the global fallback that exists for errors an ordinary error.js structurally cannot reach.

The Minimal Contract

"use client"; // Error boundaries must be Client Components

import { useEffect } from "react";

export default function Error({
  error,
  retry,
}: {
  error: Error & { digest?: string };
  retry: () => void;
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error);
  }, [error]);

  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => retry()}>Try again</button>
    </div>
  );
}

That "use client" directive at the top isn't optional stylistic preference — error boundaries are a React feature built on class-component lifecycle methods under the hood (getDerivedStateFromError, componentDidCatch), and those only exist on the client. There is no way to write a Server Component error boundary; this is a hard requirement, not a convention you can bend.

Where It Sits in the Component Tree

error.js wraps loading.js, not-found.js, page.js, and any nested layout.js files beneath it. What it explicitly does not wrap is the layout.js or template.js at the same segment level — those sit outside the boundary, one level up in the hierarchy. This is the detail that catches people off guard: put your data-fetching logic in a layout at the same level as your error.js, and a thrown error there will not be caught by that error.js. It'll bubble further up, looking for the next error boundary above it in the tree.

If you need to catch errors thrown by the root layout itself — the outermost one, the one defining <html> and <body> — a plain error.js structurally cannot help you, because there's no layout above the root layout for it to wrap. That's what global-error.js exists for (more on that below).

Props Reference

error

An Error instance, forwarded into the Client Component. What actually reaches the client differs meaningfully depending on where the error originated and which environment you're in:

  • In development, the forwarded object is serialized including its full message, for easier debugging.
  • In production, that behavior deliberately changes. Errors thrown from Client Components still show their original message. Errors thrown from Server Components show only a generic message plus an identifier — never the original message — specifically to avoid leaking sensitive implementation details (stack traces, internal variable values, query strings) to end users who have no reason to see them.

error.digest

An automatically generated hash attached to the error. Its entire purpose is correlation: since production Server Component errors are deliberately scrubbed of detail before reaching the client, digest is the thread you pull to find the actual full error in your server-side logs. Treat it as a lookup key, not a human-readable message.

retry

This is the prop most people misuse or skip entirely. retry() doesn't just hide the error UI — it attempts to genuinely re-fetch and re-render the error boundary's children from scratch. If the underlying cause was transient (a flaky network call, a database connection blip), calling retry() gives the segment a real second chance to succeed, and if it does, the fallback UI is replaced with the actual, successfully rendered content — not just dismissed.

<button onClick={() => retry()}>Try again</button>

This is meaningfully different from a plain "dismiss the error and hope for the best" reset. Design your retry buttons around the assumption that clicking them re-runs real work, including any data fetching that failed the first time.

reset

reset() also exists, and in most cases you should reach for retry() instead — it's the one that actually recovers by re-running the failed work. reset() is for the narrower case where you specifically want to clear the error boundary's state and re-render its children without re-fetching anything, which is a less common need than it might first appear.

Handling Errors in the Root Layout: global-error.js

Since a regular error.js can never catch errors from the layout or segment it's defined alongside, the root layout is structurally unreachable by any error.js in your app. global-error.js, placed directly in the root app directory, exists specifically to plug that one gap.

"use client"; // Error boundaries must be Client Components

export default function GlobalError({
  error,
  retry,
}: {
  error: Error & { digest?: string };
  retry: () => void;
}) {
  return (
    // global-error must include html and body tags
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => retry()}>Try again</button>
      </body>
    </html>
  );
}

Two things about global-error.js are easy to get wrong the first time:

It replaces the root layout entirely when active — which means it must define its own <html> and <body> tags, its own global styles, its own fonts, and any other document-level dependency your app normally relies on the root layout to provide. None of that carries over automatically. If your app has a theme toggle implemented as a class or data-theme attribute on <html>, global-error.js won't inherit it — the default fallback UI follows the OS-level color scheme instead, and if you want it to match your app's actual theme, you have to apply that logic explicitly inside the global-error component itself, since it's rendering an entirely separate document tree.

It can't export metadata. Because error boundaries must be Client Components, and metadata/generateMetadata exports are a Server Component-only feature, global-error.jsx structurally cannot use the Metadata API. If you need a <title> here, use React's built-in <title> component directly inside the JSX instead.

This same "replaces the document, no shared styles" behavior also applies to Next.js's built-in default 500 page — worth remembering if you're debugging why an error page looks unstyled compared to the rest of your app.

Building a Graceful Degradation Pattern

A more advanced pattern worth knowing about: instead of replacing the entire failed segment with a generic "something went wrong" message, you can capture the last successfully-rendered HTML before the error occurred, and keep showing that frozen snapshot with a small persistent notification bar layered on top — rather than yanking the user's screen out from under them entirely.

"use client";

import React, { Component, ErrorInfo, ReactNode } from "react";

interface ErrorBoundaryProps {
  children: ReactNode;
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
}

interface ErrorBoundaryState {
  hasError: boolean;
}

export class GracefullyDegradingErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
  private contentRef: React.RefObject<HTMLDivElement | null>;

  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false };
    this.contentRef = React.createRef();
  }

  static getDerivedStateFromError(_: Error): ErrorBoundaryState {
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    this.props.onError?.(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <>
          <div
            ref={this.contentRef}
            suppressHydrationWarning
            dangerouslySetInnerHTML={{
              __html: this.contentRef.current?.innerHTML || "",
            }}
          />
          <div className="fixed bottom-0 left-0 right-0 bg-red-600 text-white py-4 px-6 text-center">
            <p className="font-semibold">
              An error occurred during page rendering
            </p>
          </div>
        </>
      );
    }
    return <div ref={this.contentRef}>{this.props.children}</div>;
  }
}

export default GracefullyDegradingErrorBoundary;

This is a genuinely advanced technique — freezing the DOM snapshot via a ref and re-rendering it without hydration rather than discarding it — and it's worth reaching for on high-traffic, high-stakes pages where a jarring full-content swap on error would meaningfully hurt the user experience, more than for routine internal tooling where a plain fallback message is perfectly adequate.

For Errors That Aren't Tied to a Route Segment

error.js is fundamentally a route-segment-level mechanism. If you need error recovery scoped to something narrower — a single component that isn't its own segment — that's not what this file convention is for. Next.js's catchError function exists specifically for that finer-grained, component-level case; reach for that instead of trying to force a segment boundary onto a problem that isn't segment-shaped.

Version History

VersionChange
v16.3.0retry prop became stable
v16.2.0unstable_retry prop added
v15.2.0global-error also displays in development
v13.1.0global-error introduced
v13.0.0error.js introduced

Key Takeaways

BehaviorDetail
Client Component requirementError boundaries are a client-only React feature; error.js cannot be a Server Component
ScopeWraps everything below it in its segment; does not wrap its own segment's layout/template
error.message in productionOriginal message for client-thrown errors; generic + digest for server-thrown errors
retry()Genuinely re-fetches and re-renders — prefer this over reset() for recoverable errors
reset()Clears boundary state without re-fetching — a narrower, less common need
Root layout errorsOnly catchable via global-error.js, which replaces the entire document
Component-level (non-segment) errorsUse catchError, not error.js

Getting error.js right mostly comes down to respecting its boundaries — literally. It catches exactly what's beneath it in its own segment, nothing at its own level, and nothing above the root layout without global-error.js stepping in as a special case. Once that mental model is in place, the props themselves (error, digest, retry) are straightforward enough that you rarely need to revisit this reference twice.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper