cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Error handling

Catch what breaks in a route with error.tsx, let visitors try again with retry(), handle a failing root layout, and read error digests in production.

By the end, you can
  • 01
    Add an error screen to a routeWrite an error.tsx, say which errors it catches and which go past it, and add global-error.tsx for the root layout.
  • 02
    Let the visitor try againCall retry() to ask the server for the part that failed, and tell why reset() alone can't fix a Server Component error.
  • 03
    Choose between returning and throwingReturn expected problems, like a wrong form value, as values, and read an error's digest in production.

An error screen from one file

Code fails sometimes. The tea shop's orders page reads the orders from a database, and once in a while the database doesn't answer. Then getOrders() throws an error: it stops and hands a JavaScript Error to whoever called it (try...catch shows how that works). The page waits for the orders, so it can't finish either. What should the visitor see?

In Next.js, the answer is one file. Put an error.tsx in a route's folder, and when something in that route throws, Next.js shows what error.tsx returns in its place. Here is the shop with one. Like every example in this lesson, it's a real Next.js 16.4.3 project, built with next build and run with next start. An error screen is drawn by React in the browser, so this one was also clicked through in real Chrome: open /, click Orders, then click Try again. The Steps tab opens right after the click on Orders.

The orders page with an error screenNext.js
app/_data/orders/
app/orders/error.tsx
"use client"; export default function OrdersError({ retry }: { error: unknown; retry: () => void }) {  return (    <main>      <h1>Your orders did not load</h1>      <p>Something went wrong on our side.</p>      <button onClick={() => retry()}>Try again</button>    </main>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/ordersTea shop
Home Orders
Your orders did not load

Something went wrong on our side.

Try again
Focus
the link “Orders”
Scrolled
0 px down

Errors in Chrome's console during this step (1)

  1. Error: Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

Requests the page made during this step (1), without the files it loaded

  1. GET/orders200text/x-component
    rsc
    1
    next-router-state-tree
    %5B%22%22%2C%7B%22children%22%3A%5B%22orders%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2C%22refetch%22%2C49314%5D%7D%2Cnull%2Cnull%2C49254%5D%7D%2Cnull%2Cnull%2C49242%5D
    next-url
    /

app/orders/error.tsx, the file shown first, is a component like any other, with two differences. Line 1, "use client", makes it a Client Component, because React draws error screens in the browser. And Next.js passes it props: line 3 takes retry, a function, and line 8 calls it when the visitor clicks Try again. Lines 6 and 7 are the screen itself.

Now click app/_data/orders.ts. It stands in for the shop's database. Line 3 counts the calls, and line 10 throws on the first one, the way a busy database fails once and then works again. app/orders/page.tsx waits for the orders on line 4, as in Loading UI, and app/orders/loading.tsx is the loading state from that lesson.

Here is what Chrome recorded, step by step:

  • Step 2, click Orders: the page shows the links Home Orders, then Your orders did not load, Something went wrong on our side. and the Try again button. The page asked the server for the orders page. The server answered, and its answer said that getOrders() had thrown. React drew error.tsx in the page's place.
  • Step 3, click Try again: the page shows Your orders and both orders. The click asked the server for the page again, and this time the database answered.

The links stayed on the screen the whole time. They come from the root layout, which sits outside error.tsx. Only the part that failed was replaced. The Steps tab also lists an error in Chrome's console for step 2: Next.js logs every error an error screen catches. In production it reads Minified React error #441: React's general message for a failed Server Component.

Left, a card for OrdersPage, crossed out, with the note getOrders threw. An arrow goes from it to a browser window for slash orders. At the top of the window is a card with the links Home and Orders, marked root layout, stays. Below it, in a crate labeled error.tsx, are two cards: Your orders did not load, and Try again.OrdersPagegetOrders() threw/ordersHome Ordersroot layout: staysYour orders did not loadTry againerror.tsx
After the click on Orders, getOrders() threw, so the page had nothing to show. Next.js showed error.tsx in the page's place. The root layout's links stayed on the screen and kept working.
Real-life analogyA sign on one counter

When the till at the tea counter breaks, the shop doesn't close. Someone puts a sign on that one counter: closed for a moment, ask again. Customers can still walk around the shop. When the till works again, the next customer who asks is served as usual.

In real life: The shop: its door, its lights, its signs
In Next.js: The root layout: the links on every page
In real life: The tea counter, whose till just broke
In Next.js: The orders page, whose getOrders() threw
In real life: A sign on that counter: "Closed for a moment. Ask again."
In Next.js: The screen from error.tsx
In real life: Asking again, once the till works
In Next.js: The Try again button calling retry()
The main idea

error.tsx is the error screen of the route in its folder. When something inside that route throws, the visitor sees what error.tsx returns in its place, and the layouts around it stay. Its retry() asks the server for that part again.

error.tsx is a Client Component error boundary

None of this is new to React. An error boundary is a React component that catches errors thrown while the components inside it are drawn, and shows a fallback, a screen to show instead. error.tsx is a shortcut for one. Written by hand, the tree Next.js builds for /orders is roughly this:

What Next.js builds from the orders folder, written by handJSX
<RootLayout>
  <ErrorBoundary fallback={<OrdersError />}>
    <Suspense fallback={<Loading />}>
      <OrdersPage />
    </Suspense>
  </ErrorBoundary>
</RootLayout>

Line 1 is the root layout. Line 2 is an error boundary whose fallback is the component from error.tsx, here OrdersError. Line 3 is the Suspense boundary from loading.tsx, and line 4 is the page. This is a sketch to read, not code you write. It shows two things. The page and its loading state are inside the error boundary, so an error in either one shows OrdersError. The layout is outside, so it stays.

Why it must be a Client Component

React error boundaries run in the browser: they hold the error in their state, and React draws the fallback there. So error files must be Client Components, and Next.js checks this during the build. Here is the shop with line 1 of error.tsx removed. Open the Terminal tab:

error.tsx without "use client"Next.js
app/_data/orders/
app/orders/error.tsx
export default function OrdersError({ retry }: { error: unknown; retry: () => void }) {  return (    <main>      <h1>Your orders did not load</h1>      <p>Something went wrong on our side.</p>      <button onClick={() => retry()}>Try again</button>    </main>  );}
next build failed · Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ... > Build error occurredError: Turbopack build failed with 1 error:./app/orders/error.tsx:1:1Error: app/orders/error.tsx must be a Client Component. Add the "use client" directive the top of the file to resolve this issue.    Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client>  1 | export default function OrdersError({ retry }: { error: unknown; retry: () => void }) {     | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^>  2 |   return (     | ^^^^^^^^^^>  3 |     <main>     | ^^^^^^^^^^>  4 |       <h1>Your orders did not load</h1>     | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^>  5 |       <p>Something went wrong on our side.</p>     | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^>  6 |       <button onClick={() => retry()}>Try again</button>     | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^>  7 |     </main>     | ^^^^^^^^^^^>  8 |   );     | ^^^^>  9 | }     | ^  10 | Ecmascript file had an error     at ignore-listed frames

The build stops with app/orders/error.tsx must be a Client Component. Add the "use client" directive the top of the file to resolve this issue. The line is there to mark where the client part starts, as in every Client Component. Without it, the file would be a Server Component, which can't be an error boundary.

Next.js passes every error file the same props. Each row is one of them:

The props of error.tsx
PropWhat it holds
errorThe value that was thrown. From a Server Component it arrives as an Error; check it with instanceof Error before you read message or digest.
retryA function. It asks the server for the boundary's part again, then draws it. Use it for the Try again button.
resetA function. It clears the error and draws the same part again, without asking the server.

Which error.tsx catches it

An error goes up the tree until it meets an error boundary, the way a thrown error goes up through the functions that called it until a catch stops it. So the nearest error.tsx above the part that failed catches it. That includes the folders below it: app/error.tsx catches errors from every page that has no closer one. But an error.tsx doesn't cover the layout.tsx in its own folder, because the layout sits outside the boundary. The tea shop now has both app/error.tsx and app/orders/error.tsx. Pick each version and guess, before you look, which screen Chrome showed:

Playground: which file catches the error?
The tea shop's orders page, with this part failingNext.js
app/_data/orders/
app/orders/page.tsx
import { getOrders } from "../_data/orders"; export default async function OrdersPage() {  const orders = await getOrders();  return (    <main>      <h1>Your orders</h1>      <ul>        {orders.map((order) => (          <li key={order.id}>            Order {order.id}: {order.tea}          </li>        ))}      </ul>    </main>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/orders
This page couldn’t load

A server error occurred. Reload to try again.

Reload

ERROR 3281904107

Focus
nothing: the page itself
Scrolled
0 px down

Errors in Chrome's console during this step (1)

  1. Error: Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

Requests the page made during this step (2), without the files it loaded

  1. GET/orders200a whole page
  2. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /orders
Try it yourself
No error.tsx

No error.tsx anywhere. getOrders() throws on line 4, and nothing in the app catches it. Next.js's own error page takes the whole window: This page couldn’t load, with no links.

Every version was built with next build and opened at /orders in real Chrome, against next start. The Steps tab shows what Chrome showed. Reset goes back to the first version.

With no error file at all, Next.js has its own: This page couldn’t load, with a Reload button, and nothing of the shop. When the page fails, app/orders/error.tsx catches it. When the orders layout fails, the error goes past app/orders/error.tsx, which sits inside that layout, to app/error.tsx: The shop hit a problem. The orders layout is gone from the screen, because the part that failed was the layout itself.

Four crates nested inside each other. The outer one is RootLayout. Inside it, app/error.tsx. Inside that, OrdersLayout, which holds a card SignedInAs. Inside that, app/orders/error.tsx, which holds a card OrdersPage. Right of the crates, a label says the orders layout fails, caught by app/error.tsx, with an arrow to the app/error.tsx crate. A second label says the page fails, caught by app/orders/error.tsx, with an arrow to that crate.SignedInAsOrdersPageapp/orders/error.tsxOrdersLayoutapp/error.tsxRootLayoutOrders layout fails:caught by app/error.tsxPage fails:caught byapp/orders/error.tsx
The tree for /orders, from the outside in. An error goes up to the nearest error.tsx around the part that failed. app/orders/error.tsx sits inside the orders layout, so an error in that layout goes past it, to app/error.tsx.

Each row is one place where an error can happen, and what Chrome showed for it:

Where the error happens, and what the visitor sees
What failsWhat showsWhat stays on the screen
No error.tsx anywhereNext.js's own error page: This page couldn’t load.Nothing of the shop: the whole page is Next.js's.
app/orders/page.tsxapp/orders/error.tsx: Your orders did not load.The root layout's links.
app/orders/layout.tsxapp/error.tsx: The shop hit a problem.The root layout's links. The orders layout is gone.
app/layout.tsx, the root layoutapp/global-error.tsx, or Next.js's own page without one.Nothing: the root layout itself failed.
Errors in event handlers

Error boundaries catch errors thrown while React draws components, including Server Components. A click handler runs later, after drawing, so the docs say no error boundary sees an error thrown there. Catch it in the handler, keep it in state, and draw a message. An error thrown inside a Transition, an update started with startTransition that React runs without blocking other updates, is the exception: it goes up to the nearest error boundary.

Trying again: retry() and reset()

Some errors don't last: a database that was busy for a moment, a network that dropped one request. For those, a Try again button is the best error screen. Next.js gives every error file two functions for it, and they do different things. This version of the error screen has a button for each. It was opened at /orders in Chrome, where the first call to the database fails, then each button was clicked:

An error screen with reset() and retry()Next.js
app/_data/orders/
app/orders/error.tsx
"use client"; type Props = { error: unknown; retry: () => void; reset: () => void }; export default function OrdersError({ retry, reset }: Props) {  return (    <main>      <h1>Your orders did not load</h1>      <button onClick={() => reset()}>Draw it again</button>      <button onClick={() => retry()}>Try again</button>    </main>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/ordersTea shop
Home Orders
Your orders did not load
Draw it againTry again
Focus
nothing: the page itself
Scrolled
0 px down

Errors in Chrome's console during this step (1)

  1. Error: Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

Requests the page made during this step (1), without the files it loaded

  1. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /__PAGE__
    next-url
    /orders
  • reset(), line 9: it clears the error and asks React to draw the page again. React draws it from what it already has, and what it has is the server's answer that said getOrders() threw. So step 2 shows the same error screen, and asked the server for nothing: the only request it lists, if any, is a prefetch: a request the page's JavaScript makes early for a page that a link leads to.
  • retry(), line 10: it asks the server for the page again, then draws the answer. Step 3 made one request for /orders that isn't a prefetch, the server ran the page again, the database answered, and the orders showed.
Two rows. Top row: a card Draw it again, reset, with an arrow to a card the same result, nothing asked of the server, then an arrow to a card Your orders did not load, marked still failing. Bottom row: a card Try again, retry, with an arrow to a card GET slash orders, a new request, then an arrow to a card Order 1042 Green tea, marked works.reset()Draw it againthe same resultno new datathe error screenstill failingretry()Try againGET /ordersa new requestthe two ordersworks
The recorded clicks. reset() asked the server for nothing: React drew the same result again, and it failed again. retry() asked the server for the page again, and this time getOrders() answered.

So use retry() for a Try again button. The page's data comes from the server, and only a new request can bring new data. reset() only helps when the error happened in the browser and drawing again might work. The other requests in step 3 are prefetches for the links, and Chrome's console lists React error #419 twice: React's note that the server couldn't finish part of the first HTML, so the browser drew that part itself.

retry() is new in Next.js 16

Next.js 16.2 (March 2026) added retry as unstable_retry, and 16.3 (August 2026) made it stable under its own name. Before 16.2, error files only had reset, so older tutorials and projects call reset() for their Try again button. As step 2 shows, that can't bring back data from the server. In Next.js's code, retry is three lines: it refreshes the page from the server and calls reset(), both in a Transition (error-boundary.tsx, lines 111 to 116).

When the root layout fails: global-error.tsx

app/error.tsx catches errors from every page, but not from the root layout: like every error file, it sits inside the layout of its own folder. The root layout holds the <html> and <body> of every page, so when it fails, nothing is left to draw a screen in. For that case, Next.js has one more file: app/global-error.tsx. It takes the root layout's place. Here the root layout shows who is signed in, and the sign-in service is down. Pick both versions:

Playground: when the root layout fails
The tea shop, with a root layout that failsNext.js
app/_data/orders/
app/layout.tsx
import { Suspense } from "react";import Link from "next/link";import { getCustomerName } from "./_data/customer"; export const metadata = { title: "Tea shop" }; async function SignedInAs() {  const name = await getCustomerName();  return <span>Signed in as {name}</span>;} export default function RootLayout({ children }: LayoutProps<"/">) {  return (    <html lang="en">      <body>        <nav>          <Link href="/">Home</Link> <Link href="/orders">Orders</Link>{" "}          <Suspense fallback={<span>Checking who you are…</span>}>            <SignedInAs />          </Suspense>        </nav>        {children}      </body>    </html>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
This page couldn’t load

A server error occurred. Reload to try again.

Reload

ERROR 1501640012

Focus
nothing: the page itself
Scrolled
0 px down

Errors in Chrome's console during this step (1)

  1. Error: Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

Requests the page made during this step (1), without the files it loaded

  1. GET/200a whole page
Try it yourself
No global-error.tsx

The root layout's SignedInAs throws on line 8. app/error.tsx sits inside the root layout, so it can't stand in for it. Next.js's own page shows: This page couldn’t load, a Reload button, and the error's digest.

Both versions were built with next build and opened at / in real Chrome, against next start. Only app/global-error.tsx changes. Reset goes back to the first version.

Without global-error.tsx, Next.js shows its own page: This page couldn’t load, A server error occurred. Reload to try again., a Reload button, and ERROR with a number, the error's digest: a code that leads to the real message in the server's log. With it, Chrome shows the shop's own screen. Because global-error.tsx replaces the root layout, it has to bring its own <html> and <body>, as lines 5 and 6 do.

The docs list what else follows from that:

  • It doesn't get the root layout's styles and fonts. Import what it needs in the file itself.
  • It can't export metadata, because it's a Client Component. To give it a title, put React's <title> inside it.
  • It's for the root layout only. Keep an app/error.tsx for errors in pages, so the links stay.

Expected errors are values, not exceptions

Not every problem is an error to throw. The Next.js docs split them in two. Expected errors can happen while the app works as it should: a visitor types a wrong value into a form, or a tea sells out. Uncaught exceptions shouldn't happen at all: a bug, or a service that is down. Throw only the second kind, and let error.tsx catch it. Return the first kind as a value, and show it where it happened (Error strategies makes the same split in plain JavaScript).

Here is a form for ordering more Green tea, which accepts 1 to 10 boxes. The form is a Client Component that calls a Server Function, a function that runs on the server when the form is sent. A Server Function that a form calls is also called a Server Action.

app/reorder/reorder-form.tsx: the form shows what the action returnsTSX
"use client";

import { useActionState } from "react";
import { reorder } from "./actions";

export default function ReorderForm() {
  const [state, formAction, pending] = useActionState(reorder, { message: "" });
  return (
    <form action={formAction}>
      <label htmlFor="boxes">Boxes</label> <input id="boxes" name="boxes" defaultValue="1" />{" "}
      <button disabled={pending}>Order</button>
      {state.message && <p aria-live="polite">{state.message}</p>}
    </form>
  );
}

Line 7 passes the action, reorder, to useActionState. It gives back state, whatever the action returned last, starting as { message: "" }; formAction, which line 9 gives the form; and pending, which line 11 uses to turn the button off while the action runs. Line 12 shows state.message when there is one. Now the action, in two versions. Pick each one; the Steps tab opens after the click on Order:

Playground: return the problem, or throw it?
The reorder form, with this version of app/reorder/actions.tsNext.js
app/reorder/
app/reorder/actions.ts
"use server"; export async function reorder(previous: { message: string }, formData: FormData) {  const boxes = Number(formData.get("boxes"));  if (!Number.isInteger(boxes) || boxes < 1 || boxes > 10) {    return { message: "Choose between 1 and 10 boxes." };  }  // A real shop would save the order here.  return { message: `Ordered ${boxes} boxes of Green tea.` };}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/reorderTea shop
Home Order more
Order more Green tea
1 Order

Choose between 1 and 10 boxes.

Focus
nothing: the page itself
Scrolled
0 px down

Requests the page made during this step (1), without the files it loaded

  1. POST/reorder200text/x-component
    next-router-state-tree
    %5B%22%22%2C%7B%22children%22%3A%5B%22reorder%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%2C514%5D%7D%2Cnull%2Cnull%2C514%5D%7D%2Cnull%2Cnull%2C530%5D
    next-action
    60101d7e3c8968432d7d2e612d0bc77d239e97ea79
Try it yourself
Return the message

Line 6 returns the problem as a value. The form shows Choose between 1 and 10 boxes., and the page stays. The field went back to 1: after a form's action finishes without throwing, React resets the fields it doesn't control.

Both versions were built with next build and clicked through in real Chrome: open /reorder, type 40 into Boxes, click Order. The Steps tab opens after the click. Only app/reorder/actions.ts changes. Reset goes back to the first version.

In the first version, line 6 returns { message: "Choose between 1 and 10 boxes." }. The form shows the message, and the visitor can fix the number. In the second, line 6 throws the same text. The action's request answered 500, and app/error.tsx replaced the page with The shop hit a problem: no form, and no word about what was wrong. In production, the thrown message wouldn't even reach the browser: React replaces it with a general one.

Each row compares the two kinds:

Expected errors and uncaught exceptions
QuestionAn expected errorAn uncaught exception
What is it?A problem the app knows can happen: a wrong form value, a full stock, an email already in use.Something that should not happen: a bug, or a service that is down.
How does the code report it?It returns a value that says what went wrong, like { message }.It throws, or lets the error go up.
What does the visitor see?The page as it was, with a message next to the form.The nearest error.tsx, in place of that part of the page.
What reaches the browser in production?Exactly the message you returned.From a Server Component or a Server Action: a general message and a digest, and the real message stays in the server's log. From a Client Component: the value as it was thrown.

The same goes for Server Components. A page whose data request answers "not available" can return a short message instead of throwing. A thing that doesn't exist at all, like an order number nobody has, has its own function, notFound(), which the next lesson teaches.

Errors in production: a digest instead of the message

An error's message can hold things the visitor must never see: a database address, a name, a piece of a query. So in production, Next.js keeps a Server Component's error message on the server. This error.tsx shows what it was given: line 4 reads error.message, and line 5 reads error.digest. The database is down for every request now. Open the Steps tab first, then the Terminal tab:

What error.tsx gets in productionNext.js
app/_data/orders/
app/orders/error.tsx
"use client"; export default function OrdersError({ error }: { error: unknown }) {  const message = error instanceof Error ? error.message : String(error);  const digest = error instanceof Error && "digest" in error ? String(error.digest) : "none";  return (    <main>      <h1>Your orders did not load</h1>      <p>Message: {message}</p>      <p>Error code: {digest}</p>    </main>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/ordersTea shop
Home Orders
Your orders did not load

Message: Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

Error code: 1517961899

Focus
nothing: the page itself
Scrolled
0 px down

Errors in Chrome's console during this step (1)

  1. Error: Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

Requests the page made during this step (3), without the files it loaded

  1. GET/orders200a whole page
  2. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /orders
  3. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /__PAGE__
    next-url
    /orders

In the Steps tab, the message isn't The orders database did not answer. It's Minified React error #441. In production, React shortens its own messages to a number, and #441 means: "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details." Under it is Error code with a number: the digest, a short code Next.js makes from the error.

Now the Terminal tab. After the build, next start printed the real error, twice (once for the recorder's plain request and once for Chrome): ⨯ Error: The orders database did not answer., where it was thrown, and digest: with the same number. That's what the digest is for. A visitor reports the code on the screen, and you search the server's logs for it to find the real message.

Two windows. Left, a terminal for next start shows the card Error: The orders database did not answer, and the card digest 1517961899. Right, a browser window shows the error screen's cards: a message that is only React's error number 441, and Error code 1517961899. An arrow goes from the digest in the terminal to the error code in the browser, labeled only the digest travels.next startThe orders database didnot answer.digest: '1517961899'browserMessage: Minified Reacterror #441; visit…Error code: 1517961899only the digest travels
The same error, in two places. The server's log has the real message and the digest. The browser gets React's general message, error #441, and the same digest. Search the server's log for the digest to find the real message.

Three more things from this recording and the docs:

  • In development the message is there. The docs say next dev sends the real message to the browser, to help you debug. Only production builds hide it, so test your error screens with next build and next start too.
  • Client Components keep their message. An error thrown in the browser is already there, so the docs say it reaches error.tsx as it was thrown. It might not even be an Error, which is why line 4 checks error instanceof Error before it reads message.
  • The page still answered 200. Pick the Network tab: GET /orders answered 200, and its HTML holds Loading your orders…, not the error screen. The layout and the loading state were sent before the page ran, with the status, and React drew the error screen in the browser.

Error screens across the tea shop

error.tsx replaces a whole route's page. Sometimes only one part of a page should get an error screen: the orders list can fail while the tea suggestions under it still work. For that, Next.js 16.3 added catchError, from next/error (16.2 had it as unstable_catchError). It makes an error boundary that you can put around any part of a page:

An error boundary around one part of the page, with catchErrorNext.js
app/_data/orders/
app/orders/orders-boundary.tsx
"use client"; import { catchError, type ErrorInfo } from "next/error"; function OrdersFallback(props: object, { retry }: ErrorInfo) {  return (    <div>      <p>Your orders did not load.</p>      <button onClick={() => retry()}>Try again</button>    </div>  );} export default catchError(OrdersFallback);
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/ordersTea shop
Home Orders
Your orders

Your orders did not load.

Try again
Teas you might like

Lemon tea: $3 a box

Focus
nothing: the page itself
Scrolled
0 px down

Errors in Chrome's console during this step (1)

  1. Error: Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

Requests the page made during this step (3), without the files it loaded

  1. GET/orders200a whole page
  2. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /orders
  3. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /__PAGE__
    next-url
    /orders

In app/orders/orders-boundary.tsx, line 5 is the fallback. It gets two arguments: the props given to the boundary, and the same error, retry and reset an error.tsx gets. Line 9 calls retry(). Line 14 turns the fallback into a component. In app/orders/page.tsx, lines 22 to 26 put OrdersBoundary around the list only. In step 1, the list failed, and Chrome still showed the heading, the suggestions under it and the links. Step 2, Try again, brought the orders back. The docs add that catchError lets notFound() and redirect() through, which a plain React error boundary would catch by mistake.

Put together, a route's error files look like this. Each folder that needs its own screen gets an error.tsx, the root folder gets one for everything else, and global-error.tsx covers the root layout:

The tea shop's error filesText
app/
  _data/
    customer.ts
    orders.ts
  orders/
    error.tsx
    loading.tsx
    page.tsx
  error.tsx
  global-error.tsx
  layout.tsx
  page.tsx

Now sort these problems: which ones should the code return as a value, and which ones should reach an error screen?

Return it, or let it throw?
  • The visitor typed 40 boxes, and the limit is 10.
  • The email address is already used by another account.
  • Mint tea sold out while the visitor was ordering.
  • The orders database didn't answer.
  • The code reads order.tea.name, but order.tea is a string.
  • The sign-in service is down.
Try it yourself
0 of 6 correct

Sort each problem: should the code return it as a value, or let it throw to the nearest error.tsx?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "error.tsx catches errors in the layout beside it." No. It sits inside that layout. The failing orders layout went past app/orders/error.tsx to app/error.tsx.
  • "error.tsx can be a Server Component." No. Error boundaries are Client Components, and without "use client" the build stops.
  • "reset() loads the page again." No. It draws what React already has, and asked the server for nothing. retry() asks the server.
  • "In production, error.message is the message I threw." No, not for a Server Component. It's React's general message; the real one is in the server's log, next to the digest.
  • "A page that fails always answers 500." No. The recorded /orders answered 200: its layout and loading state were already on their way.
  • "Every problem should throw." No. A wrong form value is expected. Return it, and the form can show it.
  • "app/error.tsx covers the root layout." No. That's global-error.tsx's job, with its own <html> and <body>.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upWhich file catches it?

These are the shop's files. getOrders() throws while app/orders/page.tsx renders. Which file's screen does the visitor see?

Starter codeText
app/
  _data/
    customer.ts
    orders.ts
  orders/
    error.tsx
    loading.tsx
    page.tsx
  error.tsx
  layout.tsx
  page.tsx

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the missing line

    With this file as app/orders/error.tsx, next build stops. Which line is missing?

    Starter codeTSX
    export default function OrdersError({ retry }: { error: unknown; retry: () => void }) {
      return (
        <main>
          <h1>Your orders did not load</h1>
          <p>Something went wrong on our side.</p>
          <button onClick={() => retry()}>Try again</button>
        </main>
      );
    }
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAdd a Try again button

      Replace line 8 with a Try again button that brings the orders back once the database answers. Which function does it call?

      Starter codeTSX
      "use client";
      
      export default function OrdersError({ retry }: { error: unknown; retry: () => void }) {
        return (
          <main>
            <h1>Your orders did not load</h1>
            <p>Something went wrong on our side.</p>
            {/* A button that asks the server for the orders again */}
          </main>
        );
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWhich button brings the orders back?

        The database was busy for a moment and now answers again. Which of the two buttons brings the orders back?

        Starter codeTSX
        "use client";
        
        type Props = { error: unknown; retry: () => void; reset: () => void };
        
        export default function OrdersError({ retry, reset }: Props) {
          return (
            <main>
              <h1>Your orders did not load</h1>
              <button onClick={() => reset()}>Draw it again</button>
              <button onClick={() => retry()}>Try again</button>
            </main>
          );
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeA layout that fails

          This is app/orders/layout.tsx, and getCustomerName() on line 5 throws. The shop has app/error.tsx and app/orders/error.tsx. Which file's screen shows?

          Starter codeTSX
          import { Suspense } from "react";
          import { getCustomerName } from "../_data/customer";
          
          async function SignedInAs() {
            const name = await getCustomerName();
            return <p>Signed in as {name}</p>;
          }
          
          export default function OrdersLayout({ children }: LayoutProps<"/orders">) {
            return (
              <>
                <Suspense fallback={<p>Checking who you are…</p>}>
                  <SignedInAs />
                </Suspense>
                {children}
              </>
            );
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeReturn, don't throw

            With this action, a visitor who types 40 boxes loses the whole form to app/error.tsx. What should line 6 do instead?

            Starter codeTypeScript
            "use server";
            
            export async function reorder(previous: { message: string }, formData: FormData) {
              const boxes = Number(formData.get("boxes"));
              if (!Number.isInteger(boxes) || boxes < 1 || boxes > 10) {
                throw new Error("Choose between 1 and 10 boxes.");
              }
              // A real shop would save the order here.
              return { message: `Ordered ${boxes} boxes of Green tea.` };
            }
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              The error handling quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Why must error.tsx start with "use client"?

                Choose an answer to see the explanation.

              2. Question 2 of 7app/orders/layout.tsx fails, and both app/error.tsx and app/orders/error.tsx exist. Which one shows?

                Choose an answer to see the explanation.

              3. Question 3 of 7This error screen has two buttons. The page failed because the database didn't answer. Which button can bring the orders back?

                Read the code, then predictTSX
                "use client";
                
                type Props = { error: unknown; retry: () => void; reset: () => void };
                
                export default function OrdersError({ retry, reset }: Props) {
                  return (
                    <main>
                      <h1>Your orders did not load</h1>
                      <button onClick={() => reset()}>Draw it again</button>
                      <button onClick={() => retry()}>Try again</button>
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              4. Question 4 of 7The root layout fails. What shows instead?

                Choose an answer to see the explanation.

              5. Question 5 of 7A visitor types 40 boxes, and the limit is 10. What should the Server Action do?

                Choose an answer to see the explanation.

              6. Question 6 of 7In production, a Server Component throws new Error("The orders database did not answer."). What does error.message hold in error.tsx?

                Choose an answer to see the explanation.

              7. Question 7 of 7This action throws for a wrong number of boxes. What did the visitor see after Order in the recording?

                Read the code, then predictTypeScript
                "use server";
                
                export async function reorder(previous: { message: string }, formData: FormData) {
                  const boxes = Number(formData.get("boxes"));
                  if (!Number.isInteger(boxes) || boxes < 1 || boxes > 10) {
                    throw new Error("Choose between 1 and 10 boxes.");
                  }
                  // A real shop would save the order here.
                  return { message: `Ordered ${boxes} boxes of Green tea.` };
                }
                

                Choose an answer to see the explanation.

              Key takeaways

              • An error.tsx in a folder is the error screen of the route there. When something inside it throws, its screen takes that part's place, and the layouts around it stay.
              • It's a React error boundary, so it must be a Client Component: without "use client", the build stops.
              • The nearest error.tsx above the failing part catches the error. The layout in its own folder sits outside it, so a layout's error goes to the folder above.
              • retry() asks the server for the part again and draws it; reset() only draws what React already has. Use retry() for Try again.
              • global-error.tsx replaces the root layout when the root layout fails, with its own <html> and <body>.
              • Return expected problems, like a wrong form value, as values. Throw only what the visitor can't fix.
              • In production, a Server Component's message stays in the server's log. The browser gets React's general message and the same digest.

              Remember the one-liner.
              Return what you expect, throw what you don't, and let the nearest error.tsx offer a retry().

              Coming next: Not found, forbidden and unauthorized. It shows notFound(), forbidden() and unauthorized(): three functions that stop a page, show the right screen, and decide the status code.

              CompleteFrontend Clear concepts. Working examples.