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.
- 01Add 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.
- 02Let 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.
- 03Choose 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.
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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
Something went wrong on our side.
- Focus
- the link “Orders”
- Scrolled
- 0 px down
Errors in Chrome's console during this step (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
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, thenYour 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 thatgetOrders()had thrown. React drewerror.tsxin the page's place. - Step 3, click Try again: the page shows
Your ordersand 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.
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.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()
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:
<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:
app/orders/error.tsxexport 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▲ 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 framesThe 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:
| Prop | What it holds |
|---|---|
error | The value that was thrown. From a Server Component it arrives as an Error; check it with instanceof Error before you read message or digest. |
retry | A function. It asks the server for the boundary's part again, then draws it. Use it for the Try again button. |
reset | A 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:
app/orders/page.tsximport { 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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
A server error occurred. Reload to try again.
ERROR 3281904107
- Focus
- nothing: the page itself
- Scrolled
- 0 px down
Errors in Chrome's console during this step (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
GET/orders200a whole pageGET/200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /_tree
- next-url
- /orders
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.
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.
/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:
| What fails | What shows | What stays on the screen |
|---|---|---|
No error.tsx anywhere | Next.js's own error page: This page couldn’t load. | Nothing of the shop: the whole page is Next.js's. |
app/orders/page.tsx | app/orders/error.tsx: Your orders did not load. | The root layout's links. |
app/orders/layout.tsx | app/error.tsx: The shop hit a problem. | The root layout's links. The orders layout is gone. |
app/layout.tsx, the root layout | app/global-error.tsx, or Next.js's own page without one. | Nothing: the root layout itself failed. |
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:
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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
- Focus
- nothing: the page itself
- Scrolled
- 0 px down
Errors in Chrome's console during this step (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
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 saidgetOrders()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/ordersthat isn't a prefetch, the server ran the page again, the database answered, and the orders showed.
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.
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:
app/layout.tsximport { 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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
A server error occurred. Reload to try again.
ERROR 1501640012
- Focus
- nothing: the page itself
- Scrolled
- 0 px down
Errors in Chrome's console during this step (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
GET/200a whole page
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.
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.tsxfor 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.
"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:
app/reorder/actions.tsNext.jsapp/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.` };}Each step was taken in real Chrome against next start. Pick one to see the page after it.
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
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
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.
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:
| Question | An expected error | An 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:
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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
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)
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
GET/orders200a whole pageGET/200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /_tree
- next-url
- /orders
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.
Three more things from this recording and the docs:
- In development the message is there. The docs say
next devsends the real message to the browser, to help you debug. Only production builds hide it, so test your error screens withnext buildandnext starttoo. - Client Components keep their message. An error thrown in the browser is already there, so the docs say it reaches
error.tsxas it was thrown. It might not even be anError, which is why line 4 checkserror instanceof Errorbefore it readsmessage. - The page still answered 200. Pick the Network tab:
GET /ordersanswered200, and its HTML holdsLoading 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:
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);Each step was taken in real Chrome against next start. Pick one to see the page after it.
Your orders did not load.
Lemon tea: $3 a box
- Focus
- nothing: the page itself
- Scrolled
- 0 px down
Errors in Chrome's console during this step (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
GET/orders200a whole pageGET/200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /_tree
- next-url
- /orders
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:
app/
_data/
customer.ts
orders.ts
orders/
error.tsx
loading.tsx
page.tsx
error.tsx
global-error.tsx
layout.tsx
page.tsxNow sort these problems: which ones should the code return as a value, and which ones should reach an error screen?
- 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, butorder.teais a string. - The sign-in service is down.
Sort each problem: should the code return it as a value, or let it throw to the nearest error.tsx?
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.tsxtoapp/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
/ordersanswered200: 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.
These are the shop's files. getOrders() throws while app/orders/page.tsx renders. Which file's screen does the visitor see?
app/
_data/
customer.ts
orders.ts
orders/
error.tsx
loading.tsx
page.tsx
error.tsx
layout.tsx
page.tsxapp/orders/error.tsx. It's the nearest error boundary above the page. The recorded version showed its screen, Your orders did not load, under the root layout's links.
With this file as app/orders/error.tsx, next build stops. Which line is missing?
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>
);
}
"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>
);
}
"use client"; on line 1. Error boundaries must be Client Components. Without it, the recorded build stops: app/orders/error.tsx must be a Client Component.
Replace line 8 with a Try again button that brings the orders back once the database answers. Which function does it call?
"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>
);
}
"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>
);
}
retry(). <button onClick={() => retry()}>Try again</button> asks the server for the orders page again. In the recording, the second request found the database answering, and the orders showed.
The database was busy for a moment and now answers again. Which of the two buttons brings the orders back?
"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>
);
}
Try again, which calls retry(). Draw it again calls reset(), which draws the same failed result: the recorded click asked the server for nothing, and the error screen came back. Try again asked the server for the page, and the orders showed.
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?
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}
</>
);
}
app/error.tsx. app/orders/error.tsx sits inside this layout, so it can't catch the layout's own error. The recording showed The shop hit a problem, under the root layout's links.
With this action, a visitor who types 40 boxes loses the whole form to app/error.tsx. What should line 6 do instead?
"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.` };
}
"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.` };
}
Return the message: return { message: "Choose between 1 and 10 boxes." };. useActionState makes it the new state, and the form shows it. The recorded version showed the message under the form, instead of The shop hit a problem.
Check your understanding
Question 1 of 7Why must
error.tsxstart with"use client"?Choose an answer to see the explanation.
Question 2 of 7
app/orders/layout.tsxfails, and bothapp/error.tsxandapp/orders/error.tsxexist. Which one shows?Choose an answer to see the explanation.
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.
Question 4 of 7The root layout fails. What shows instead?
Choose an answer to see the explanation.
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.
Question 6 of 7In production, a Server Component throws
new Error("The orders database did not answer."). What doeserror.messagehold inerror.tsx?Choose an answer to see the explanation.
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.tsxin 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.tsxabove 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. Useretry()for Try again.global-error.tsxreplaces 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.