Skip to the solution
MediumFrontend9 min read

What is error.js in Next.js and how does error handling work in the App Router?

733 views

The problem

Question presented to candidate: "A Server Component on our orders page sometimes throws because an upstream API is flaky. What does the user see by default, how do you show a friendly error with a retry button, and what ends up in the browser versus the server logs?"

What a strong answer should cover:

  • error.tsx wraps its segment (page, loading, not-found and nested layouts) in a React error boundary and renders as the fallback; it must be a Client Component.
  • It does not catch errors from the layout.tsx or template.tsx of its own segment; those bubble to the parent boundary, and the root layout needs global-error.tsx with its own html and body.
  • Props in 16.x: error (typed unknown; narrow before reading), retry() which re-fetches and re-renders the segment (stable since 16.3), and reset() which only re-renders.
  • In production, Server Component errors reach the browser with a generic message and a digest that matches the full error in the server log; Client Component errors keep their message.
  • Event handler and async errors outside rendering are not caught by error boundaries; handle them with try/catch or state.
  • Expected failures (missing record, no access) use notFound(), forbidden() or returned values, not thrown errors; catchError gives component-level boundaries.

Clarifying questions expected:

  • "Should the navigation stay usable when the content fails?" β€” decides which segment gets the error.tsx.
  • "Is the failure expected or a bug?" β€” expected cases should be modelled as data or a 404, not as an error.

Code / implementation expected: Yes β€” a segment error.tsx using retry() and the digest, a catchError boundary for one widget, and global-error.tsx.

  • nextjs
  • error-handling
  • error-boundary
  • app-router

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 8 sections

Read the code

Segment error.tsx with retry, a widget boundary, and global-error
// app/orders/error.tsx
"use client"; // error boundaries must be Client Components

import { useEffect } from "react";

export default function OrdersError({ error, retry }: { error: unknown; retry: () => void }) {
  const digest = error instanceof Error ? (error as Error & { digest?: string }).digest : undefined;

  useEffect(() => {
    // Send to your error tracker. In production a server error arrives with a generic
    // message; the digest matches the full error in the server log.
    console.error("orders segment failed", digest ?? error);
  }, [error, digest]);

  return (
    <div role="alert">
      <h2>We could not load your orders.</h2>
      {digest && <p>Reference: {digest}</p>}
      {/* retry() re-fetches the segment from the server and re-renders it (stable since 16.3). */}
      <button onClick={() => retry()}>Try again</button>
    </div>
  );
}

// app/lib/orders.ts
export async function getOrder(id: string) {
  const res = await fetch(`https://api.example.com/orders/${id}`, { cache: "no-store" });
  if (!res.ok) throw new Error(`orders API returned ${res.status}`); // caught by orders/error.tsx
  return (await res.json()) as { id: string; total: number };
}

// app/orders/[id]/page.tsx
import { getOrder } from "../../lib/orders";
import RecommendationsBoundary from "../../components/RecommendationsBoundary";
import Recommendations from "../../components/Recommendations";

export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {
  const { id } = await params;
  const order = await getOrder(id);
  return (
    <>
      <h1>Order {order.id}: {order.total}</h1>
      {/* A failure here should not take the whole order page down. */}
      <RecommendationsBoundary title="Recommendations">
        <Recommendations />
      </RecommendationsBoundary>
    </>
  );
}

// app/components/RecommendationsBoundary.tsx
"use client";
import { catchError, type ErrorInfo } from "next/error";

// Component-level boundary (catchError is stable since 16.3). Unlike a hand-written
// class boundary, it lets notFound() and redirect() pass through and clears on navigation.
function Fallback(props: { title: string }, { retry }: ErrorInfo) {
  return (
    <aside>
      <p>{props.title} are unavailable right now.</p>
      <button onClick={() => retry()}>Retry</button>
    </aside>
  );
}

export default catchError(Fallback);

// app/components/Recommendations.tsx
export default async function Recommendations() {
  const res = await fetch("https://api.example.com/recommendations", { cache: "no-store" });
  if (!res.ok) throw new Error("recommendations failed");
  const items: string[] = await res.json();
  return <ul>{items.map((i) => <li key={i}>{i}</li>)}</ul>;
}

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

// Replaces the root layout when it (or the root template) throws, so it brings its own html/body.
export default function GlobalError({ retry }: { error: unknown; retry: () => void }) {
  return (
    <html lang="en">
      <body>
        <h1>Something went wrong</h1>
        <button onClick={() => retry()}>Try again</button>
      </body>
    </html>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 54 of 129 in Next.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Next.js questions
Up nextMedium

What is the difference between build-time and runtime environment variables in Next.js?

Continue