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

Not found, forbidden and unauthorized

Stop a page with notFound(), forbidden() or unauthorized(), show not-found.tsx, forbidden.tsx or unauthorized.tsx, and see when 404, 403 or 401 is sent.

By the end, you can
  • 01
    Stop a page that can't be shownCall notFound(), forbidden() or unauthorized(), and say which file Next.js shows for each one.
  • 02
    Pick the right oneTell a thing that doesn't exist from a visitor who isn't signed in, and from one who isn't allowed.
  • 03
    Predict the status codeSay when a page answers 404, 403 or 401, and when it answers 200 with a noindex tag instead.

A page for an order that isn't there

Every tea shop order has a page: order 1042 is at /orders/1042. One folder answers them all, app/orders/[id], as Dynamic routes showed. But a visitor can type any number. What should /orders/9999 show, when no order has that number?

Not an error screen: nothing broke. The order just doesn't exist. For that, Next.js has a function, notFound(), and a file, not-found.tsx. Here is the shop with both. 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. It was also opened in real Chrome: this order page sends its first part before it looks for the order, so its not-found screen is finished in the browser, and only Chrome can show it. The Steps tab opens on /orders/9999:

An order page that calls notFound()Next.js
app/_data/orders/[id]/
app/orders/[id]/page.tsx
import { notFound } from "next/navigation";import { getOrder } from "../../_data/orders"; export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {  const { id } = await params;  const order = await getOrder(id);  if (!order) notFound();  return (    <main>      <h1>Order {order.id}</h1>      <p>        {order.boxes} boxes of {order.tea}      </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/orders/9999Tea shop
Tea shop
Order not found

Check the number on your receipt.

Back to the shop
Focus
nothing: the page itself
Scrolled
0 px down

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

  1. GET/orders/9999200a whole page
  2. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /orders/9999

Here is app/orders/[id]/page.tsx, line by line:

  • Line 1 imports notFound from next/navigation.
  • Line 5 waits for params and takes the order's number, id, out of it. For /orders/9999, id is "9999".
  • Line 6 looks for the order with getOrder, from app/_data/orders.ts, a stand-in for the shop's database with two orders, 1042 and 1043.
  • Line 7: if there's no order, notFound() stops the page. It works by throwing, so nothing after it runs, and you don't write return in front of it. TypeScript knows this too: on lines 10 and 12, order can't be undefined.

Now click app/orders/[id]/not-found.tsx. It's an ordinary component that takes no props: line 6 is the heading Order not found, and line 8 links back to the shop. The loading.tsx beside it is the loading state from Loading UI: the order page waits for params, and with no list of order numbers during the build, Next.js only learns the number when a request comes in.

In the Steps tab, step 1 opened /orders/9999 and showed Order not found, Check the number on your receipt. and the link, under the root layout's Tea shop link. The address stayed /orders/9999. Step 2 opened /orders/1042, which showed Order 1042 and 2 boxes of Green tea. Now open the Network tab and pick /orders/9999. Two things there may surprise you: the status is 200, not 404, and the HTML holds only Loading your order…, plus a tag, <meta name="robots" content="noindex">. Both come from streaming, sending a page in parts, as in Loading UI: the layout and the loading state went out first, with status 200, before the page ran and called notFound(). React then drew not-found.tsx in the browser.

Left, a tag for slash orders slash 9999, and under it a card for getOrder, which finds nothing, so a card notFound is shaded. An arrow goes from it to a browser window. In the window, a card with the link Tea shop is marked root layout, stays. Below it, in a crate labeled not-found.tsx, are the cards Order not found and Check the number on your receipt./orders/9999getOrder: nothingnotFound()/orders/9999Tea shoproot layout: staysOrder not foundCheck the number on your receipt.not-found.tsx
getOrder("9999") found nothing, so line 7 called notFound(). The page stopped there, and Next.js showed the nearest not-found.tsx in its place. The root layout's link stayed.
Real-life analogyA parcel counter

At a parcel counter, you give a number and get your parcel. When something's wrong, the clerk doesn't close the shop. They give one of three short answers: no parcel has that number, show me your ID first, or this one isn't yours. Each answer tells you what to do next.

In real life: "No parcel has that number."
In Next.js: notFound(), then not-found.tsx
In real life: "Show me your ID first."
In Next.js: unauthorized(), then unauthorized.tsx
In real life: "I know who you are, but this parcel is someone else's."
In Next.js: forbidden(), then forbidden.tsx
In real life: The counter, the queue and the shop stay open
In Next.js: The layouts stay on the screen
The main idea

notFound(), forbidden() and unauthorized() stop a page that can't be shown. Next.js shows the nearest not-found.tsx, forbidden.tsx or unauthorized.tsx in its place, inside the layouts. Called before the response starts, they send 404, 403 or 401; called after, the status stays 200 and Next.js adds a noindex tag.

Which not-found page shows

notFound() goes up the folders to the nearest not-found.tsx, the way an error goes up to the nearest error.tsx. In the order's folder, the docs place not-found.tsx inside the loading state and the error screen: the layout is outside all three, so it stays. Move the file, and guess which screen Chrome shows for /orders/9999:

Playground: which not-found page shows?
The order pages, with not-found.tsx in this placeNext.js
app/_data/orders/[id]/
app/orders/[id]/not-found.tsx
import Link from "next/link"; export default function OrderNotFound() {  return (    <main>      <h1>Order not found</h1>      <p>Check the number on your receipt.</p>      <Link href="/">Back to the shop</Link>    </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/9999Tea shop
Tea shop
Order not found

Check the number on your receipt.

Back to the shop
Focus
nothing: the page itself
Scrolled
0 px down

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

  1. GET/orders/9999200a whole page
  2. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /orders/9999
Try it yourself
In the order's folder

app/orders/[id]/not-found.tsx is the closest one, so Chrome shows its heading, Order not found, under the root layout's link.

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

With the file in the order's folder, its own screen shows. With only app/not-found.tsx, that one shows: Not on the menu. With none at all, Next.js uses its own, still inside the root layout: 404, This page could not be found., and the tab's title changes to 404: This page could not be found.

app/not-found.tsx has a second job. It also answers addresses that match no route at all, like /oolong, as Project structure showed. Pick Only in app and open the Network tab: /oolong answered 404, and its HTML already holds Not on the menu. Next.js knew before it rendered anything that no route matched. Next.js also has an experimental global-not-found.tsx, for apps with no single root layout; the docs explain it.

Don't catch it

Because notFound() throws, a try...catch around it stops it, and the not-found page never shows. The docs suggest calling unstable_rethrow(error) first in such a catch, which throws Next.js's own errors again. An error.tsx doesn't catch it either: Next.js's error boundary throws its own errors again, so they reach the right file (error-boundary.tsx, lines 60 to 64).

forbidden(): signed in, but not allowed

Orders are private. Asha may see her order, 1042, but not Ben's, 1043. When the shop knows who the visitor is, and that visitor may not see the page, call forbidden(). Next.js shows forbidden.tsx. Its status code, when it can send one, is 403 Forbidden.

forbidden() and unauthorized() are still experimental in Next.js 16.4.3. To use them, turn them on in next.config.ts, the file Project structure introduced:

next.config.ts with forbidden() and unauthorized() turned onTypeScript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  cacheComponents: true,
  partialPrefetching: true,
  experimental: {
    authInterrupts: true,
  },
};

export default nextConfig;

Lines 7 to 9 are the new part: experimental: { authInterrupts: true }. Lines 5 and 6 were already there, as create-next-app writes them. Here is the shop with sign-in added. Click through its files, then read the Steps tab:

Order pages that check who's askingNext.js
app/_data/orders/[id]/sign-in/
app/orders/[id]/page.tsx
import { forbidden, notFound, unauthorized } from "next/navigation";import { getCustomer, getOrder } from "../../_data/orders"; export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {  const customer = await getCustomer();  if (!customer) unauthorized();  const { id } = await params;  const order = await getOrder(id);  if (!order) notFound();  if (order.customer !== customer) forbidden();  return (    <main>      <h1>Order {order.id}</h1>      <p>        {order.boxes} boxes of {order.tea}      </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/orders/1043Tea shop
Tea shop
This order is not yours

You are signed in, but the order belongs to another customer.

Focus
nothing: the page itself
Scrolled
0 px down

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

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

In app/_data/orders.ts, line 15 reads the visitor's name from a cookie called customer: a stand-in for real sign-in, which Authentication teaches. Each order has a customer, on lines 4 and 5. In app/orders/[id]/page.tsx, line 10 compares them: if the order isn't this customer's, forbidden() stops the page. Step 5, signed in as Asha, opened /orders/1043 and showed app/forbidden.tsx: This order is not yours. Step 4 opened her own order, 1042, which showed as usual.

Without the setting, the function doesn't work. Here is the same shop with the next.config.ts that create-next-app writes, asked for /orders/1043 as Asha. Open the Terminal tab and read what next start printed:

forbidden() without experimental.authInterruptsNext.js
app/_data/orders/[id]/sign-in/
app/orders/[id]/page.tsx
import { forbidden, notFound, unauthorized } from "next/navigation";import { getCustomer, getOrder } from "../../_data/orders"; export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {  const customer = await getCustomer();  if (!customer) unauthorized();  const { id } = await params;  const order = await getOrder(id);  if (!order) notFound();  if (order.customer !== customer) forbidden();  return (    <main>      <h1>Order {order.id}</h1>      <p>        {order.boxes} boxes of {order.tea}      </p>    </main>  );}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (5/5)  Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found├   /orders/[id]│ └ ◐ /orders/[id]└ ○ /sign-in ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ Ready⨯ Error: `forbidden()` is experimental and only allowed to be enabled when `experimental.authInterrupts` is enabled.    at d (.next/server/chunks/ssr/_0wxhkcy8ti8qe._.js:1:654)    at h (.next/server/chunks/ssr/_0wxhkcy8ti8qe._.js:1:456) {  digest: '1905272039'}⨯ Error: `forbidden()` is experimental and only allowed to be enabled when `experimental.authInterrupts` is enabled.    at d (.next/server/chunks/ssr/_0wxhkcy8ti8qe._.js:1:654)    at h (.next/server/chunks/ssr/_0wxhkcy8ti8qe._.js:1:456) {  digest: '2320678624'}

The build passed, but the request failed: Error: `forbidden()` is experimental and only allowed to be enabled when `experimental.authInterrupts` is enabled. An ordinary error: like any thrown error, it goes to the nearest error screen, not to forbidden.tsx. With the setting on, the build of the example above lists it: Experiments (use with caution): and ✓ authInterrupts.

unauthorized(): sign in first

forbidden() needs to know who the visitor is. Before that, there's an earlier question: is anyone signed in at all? If not, call unauthorized(). Next.js shows unauthorized.tsx, and its status code, when it can send one, is 401 Unauthorized: sign in, and you might be let in. The same example, from its first step:

Signed out, then signed inNext.js
app/_data/orders/[id]/sign-in/
app/orders/[id]/page.tsx
import { forbidden, notFound, unauthorized } from "next/navigation";import { getCustomer, getOrder } from "../../_data/orders"; export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {  const customer = await getCustomer();  if (!customer) unauthorized();  const { id } = await params;  const order = await getOrder(id);  if (!order) notFound();  if (order.customer !== customer) forbidden();  return (    <main>      <h1>Order {order.id}</h1>      <p>        {order.boxes} boxes of {order.tea}      </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/orders/1042Tea shop
Tea shop
Sign in to see your order
Sign in
Focus
nothing: the page itself
Scrolled
0 px down

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

  1. GET/orders/1042200a whole page
  2. GET/200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /orders/1042
  3. GET/sign-in200text/x-component
    rsc
    1
    next-router-prefetch
    1
    next-router-segment-prefetch
    /_tree
    next-url
    /orders/1042
  • Step 1, open /orders/1042: nobody is signed in, so getCustomer() on line 5 finds no cookie, and line 6 calls unauthorized(). Chrome shows app/unauthorized.tsx: Sign in to see your order and a Sign in link.
  • Steps 2 and 3, sign in: the link goes to /sign-in, and the button there stores the cookie.
  • Step 4, open /orders/1042 again: now line 5 finds "asha", and Asha sees her order.
app/sign-in/sign-in-button.tsx: a pretend sign-inTSX
"use client";

import { useState } from "react";

// A pretend sign-in: it only stores the name in a cookie. A real shop checks a password first.
export default function SignInButton() {
  const [signedIn, setSignedIn] = useState(false);

  function signIn() {
    document.cookie = "customer=asha; path=/";
    setSignedIn(true);
  }

  return signedIn ? <p>Signed in as Asha</p> : <button onClick={signIn}>Sign in as Asha</button>;
}

Line 10 sets the cookie customer=asha with document.cookie, line 11 remembers that the visitor signed in, and line 14 shows Signed in as Asha. A real shop checks a password first, as the comment on line 5 says. The Steps tab also lists React error #419 twice in step 2: React's note that the server couldn't finish the order's part of the first HTML, so the browser drew it.

Put together, the order page asks three questions, in order. The first "no" stops it:

Three questions down the left: Signed in? Order exists? Asha's order? From each, an arrow labeled no goes right to a shaded card with the function it calls, unauthorized, notFound and forbidden, and then to the file that shows: unauthorized.tsx, not-found.tsx and forbidden.tsx. Under the last question, a card says yes to all three: the order page.Signed in?nounauthorized()unauthorized.tsxOrder exists?nonotFound()not-found.tsxAsha's order?noforbidden()forbidden.tsxyes to all three: the order
The order page asks three questions, in the order of lines 6, 9 and 10. The first "no" calls a function, which stops the page, and Next.js shows the matching file. Only a visitor who passes all three sees the order.

The order of the checks matters. Line 6 comes first, so a visitor who isn't signed in can't learn whether order 9999 exists. The docs add one limit: forbidden() and unauthorized() can't be called in the root layout.

Status codes and search engines

A status code is the number at the start of every response: 200 means here is the page, 404 not found, 403 forbidden, 401 sign in first. Search engines read it: a page that answers 404 is dropped from their results. So why did every order page answer 200, even the ones that stopped?

Because of streaming, from Loading UI. The status code is sent first, together with the first part of the page. The order page waits for params, so Next.js sent the layout and Loading your order… at once, with 200. When the page ran and called unauthorized(), the status had already left. Next.js can't take it back. What it can still do is add the tag <meta name="robots" content="noindex">, which tells search engines not to keep the page in their results. React then draws unauthorized.tsx in the browser, which is why the recorded HTML only held the loading state.

A check that runs before anything is sent gets the real status. One place is the build: a page that calls notFound() while next build makes it. The shop's sale page only exists while a sale is on:

app/sale/page.tsx: a page that only exists during a saleTSX
import { notFound } from "next/navigation";
import { getSale } from "../_data/sale";

export default async function SalePage() {
  const sale = await getSale();
  if (!sale) notFound();
  return (
    <main>
      <h1>{sale.title}</h1>
      <p>{sale.discount}% off every tea</p>
    </main>
  );
}

Line 5 asks for the sale, and right now there's none, so line 6 calls notFound(). This page reads nothing from the request, so it runs during the build. Compare both cases:

Playground: when does the check run?
A page that stops, and the status it answered withNext.js
app/_data/sale/
app/sale/page.tsx
import { notFound } from "next/navigation";import { getSale } from "../_data/sale"; export default async function SalePage() {  const sale = await getSale();  if (!sale) notFound();  return (    <main>      <h1>{sale.title}</h1>      <p>{sale.discount}% off every tea</p>    </main>  );}
Recorded from Next.js 16.4.3

GET /sale404

Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-cache HIT
x-nextjs-prerender 1
x-nextjs-stale-time 300
X-Powered-By Next.js
Cache-Control s-maxage=31536000
Content-Type text/html; charset=utf-8

The HTML that arrived, one element per line, with the code of its 15 scripts left out

<!DOCTYPE html>
<html id="__next_error__">
  <head>
    <link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/1x6obpmlu1_c2.js">
    <script src="/_next/static/chunks/06bs-nf19b2js.js" async=""></script>
    <script src="/_next/static/chunks/2ki52vdnbo2ft.js" async=""></script>
    <script src="/_next/static/chunks/3l_qmp-61z4yc.js" async=""></script>
    <script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" async=""></script>
    <script src="/_next/static/chunks/3oh00mkbkq8u6.js" async=""></script>
    <meta name="robots" content="noindex">
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
  </head>
  <body>
    <script id="_R_">…</script>
    <script src="/_next/static/chunks/1x6obpmlu1_c2.js" async=""></script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
  </body>
</html>
Try it yourself
During the build

There's no sale, so line 6 calls notFound() while next build makes the page. Nothing had been sent yet, so the saved page answers 404, with the noindex tag.

Both were built with next build and asked for by the recorder against next start. The Network tab shows each response's status, its headers and its HTML. Reset goes back to the first version.

/sale answered 404, with the noindex tag, and the Terminal tab lists it as ○ /sale: the build saved the not-found result, and every visitor gets it. Its HTML is an almost empty document with the noindex tag; in Chrome, React drew Not on the menu from it. The order page answered 200, with the noindex tag. Each row is one way a page can be missing, and what the response says:

When the check runs, and what the response says
When it runsWhat the response saysWhat search engines see
During next build, like /saleNothing has been sent yet. The saved page answers 404, with the noindex tag.The real status: the page isn't there.
An address with no route, like /oolongNext.js knows before it renders anything, so it answers 404.The real status: the page isn't there.
For each request, after the first part was sent, like /orders/1042 signed outThe status 200 already went out with the layout and the loading state. Next.js adds the noindex tag.A 200 page that asks not to be kept in search results.
Two rows. Top row, slash sale: during next build, the page calls notFound, so the saved page is a card 404, then a card plus noindex. Bottom row, slash orders slash 1042, signed out: the first part goes out at once as a card 200, with the layout and Loading your order. Then the page runs and calls unauthorized, too late to change the status, so the last card says noindex added, still 200./sale, during next buildnotFound()nothing sent yetsaved: 404+ noindex/orders/1042, signed out, for each requestsent at once: 200layout, Loading your order…unauthorized()too late for the status+ noindexstill 200
The status code travels with the first part of the response. /sale called notFound() during the build, before anything was sent: 404. The order page called unauthorized() after the first part had left with 200, so Next.js could only add the noindex tag.

Is a 200 with noindex a problem? The docs say some search engines call it a "soft 404", and the noindex tag keeps it out of their results. For private pages like orders, search engines can't see them anyway: they never sign in. When a team still needs the real 401, 403 or 404 for each request, the docs suggest running the check before the page renders, in proxy.ts, which the proxy.ts lesson teaches. And Dynamic routes showed a third case: a tea page whose slug the build didn't list answered 200 the first time and 404 after that, once Next.js had saved the result.

Choosing the right one

The three functions answer three different problems. Each row is one of them, the file that shows, and the status code it stands for:

notFound(), unauthorized() and forbidden()
CallWhenShowsStatus, before streaming
notFound()The thing doesn't exist: no order with this number.not-found.tsx404 Not Found
unauthorized()We don't know who the visitor is: nobody is signed in.unauthorized.tsx401 Unauthorized
forbidden()We know who it is, and they may not see it: someone else's order.forbidden.tsx403 Forbidden

The shop's order pages, ready to ship, have one of each file: not-found.tsx in the order's folder, because its message is about orders, and forbidden.tsx and unauthorized.tsx in app, for every page that checks who's asking:

The order pages, ready to shipText
app/
  _data/
    orders.ts
  orders/
    [id]/
      loading.tsx
      not-found.tsx
      page.tsx
  sign-in/
    page.tsx
    sign-in-button.tsx
  forbidden.tsx
  layout.tsx
  page.tsx
  unauthorized.tsx
next.config.ts

Now sort these visits: which function should the page call?

notFound(), unauthorized() or forbidden()?
  • Asha types /orders/9999, and no order has that number.
  • Someone who isn't signed in opens /orders/1042.
  • Asha, signed in, opens Ben's order, /orders/1043.
  • A guide page for a tea the shop stopped selling, whose text was deleted.
  • A visitor whose sign-in cookie ran out opens their orders.
  • Ben, signed in as a customer, opens the staff's stock page.
Try it yourself
0 of 6 correct

Sort each visit: which function should the page call?

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

Common mix-ups

  • "notFound() always sends a 404." Only before the response starts. The recorded /orders/9999 answered 200, with a noindex tag.
  • "You have to write return notFound()." No. It throws, so nothing after it runs.
  • "error.tsx catches notFound()." No. Next.js passes its own errors on to the right file.
  • "forbidden() works like notFound() in every project." No, not in 16.4.3. It needs experimental.authInterrupts, or it throws an ordinary error.
  • "401 and 403 mean the same thing." No. 401: sign in first. 403: signed in, and still not allowed.
  • "Without a not-found.tsx in the folder, the visitor gets a blank page." No. The next one up shows, or Next.js's own.

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 heading shows?

These are the overview's files. A visitor opens /orders/9999. Which heading does the page show?

Starter codeText
app/
  _data/
    orders.ts
  orders/
    [id]/
      loading.tsx
      not-found.tsx
      page.tsx
  layout.tsx
  page.tsx

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

    Exercise 2 · Warm-upWhich check stops it?

    Asha, signed in, opens /orders/1043, Ben's order. Which function stops the page?

    Starter codeTSX
    import { forbidden, notFound, unauthorized } from "next/navigation";
    import { getCustomer, getOrder } from "../../_data/orders";
    
    export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {
      const customer = await getCustomer();
      if (!customer) unauthorized();
      const { id } = await params;
      const order = await getOrder(id);
      if (!order) notFound();
      if (order.customer !== customer) forbidden();
      return (
        <main>
          <h1>Order {order.id}</h1>
          <p>
            {order.boxes} boxes of {order.tea}
          </p>
        </main>
      );
    }
    

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

      Exercise 3 · PracticeAdd the check

      Line 7 is only a comment. Write the line that shows the not-found page when no order has this number.

      Starter codeTSX
      import { notFound } from "next/navigation";
      import { getOrder } from "../../_data/orders";
      
      export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {
        const { id } = await params;
        const order = await getOrder(id);
        // If no order has this number, show the not-found page
        return (
          <main>
            <h1>Order {order.id}</h1>
            <p>
              {order.boxes} boxes of {order.tea}
            </p>
          </main>
        );
      }
      

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

        Exercise 4 · PracticeWhy does forbidden() throw?

        In a new project, this page fails for Asha on /orders/1043 with an ordinary error instead of showing forbidden.tsx. What does next.config.ts need?

        Starter codeTSX
        import { forbidden, notFound, unauthorized } from "next/navigation";
        import { getCustomer, getOrder } from "../../_data/orders";
        
        export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {
          const customer = await getCustomer();
          if (!customer) unauthorized();
          const { id } = await params;
          const order = await getOrder(id);
          if (!order) notFound();
          if (order.customer !== customer) forbidden();
          return (
            <main>
              <h1>Order {order.id}</h1>
              <p>
                {order.boxes} boxes of {order.tea}
              </p>
            </main>
          );
        }
        

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

          Exercise 5 · ChallengeThe status of a streamed page

          Nobody is signed in, and this page sits next to a loading.tsx. Which status does /orders/1042 answer with?

          Starter codeTSX
          import { forbidden, notFound, unauthorized } from "next/navigation";
          import { getCustomer, getOrder } from "../../_data/orders";
          
          export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {
            const customer = await getCustomer();
            if (!customer) unauthorized();
            const { id } = await params;
            const order = await getOrder(id);
            if (!order) notFound();
            if (order.customer !== customer) forbidden();
            return (
              <main>
                <h1>Order {order.id}</h1>
                <p>
                  {order.boxes} boxes of {order.tea}
                </p>
              </main>
            );
          }
          

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

            Exercise 6 · ChallengeThe status of a page made during the build

            There's no sale, so getSale() returns null. Which status does /sale answer with?

            Starter codeTSX
            import { notFound } from "next/navigation";
            import { getSale } from "../_data/sale";
            
            export default async function SalePage() {
              const sale = await getSale();
              if (!sale) notFound();
              return (
                <main>
                  <h1>{sale.title}</h1>
                  <p>{sale.discount}% off every tea</p>
                </main>
              );
            }
            

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

              Check your understanding

              The not found, forbidden and unauthorized quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What happens when the order page calls notFound()?

                Choose an answer to see the explanation.

              2. Question 2 of 7The order's folder has no not-found.tsx, but app/not-found.tsx exists. What does /orders/9999 show?

                Choose an answer to see the explanation.

              3. Question 3 of 7Asha is signed in and opens Ben's order. Which line of this page stops it?

                Read the code, then predictTSX
                import { forbidden, notFound, unauthorized } from "next/navigation";
                import { getCustomer, getOrder } from "../../_data/orders";
                
                export default async function OrderPage({ params }: PageProps<"/orders/[id]">) {
                  const customer = await getCustomer();
                  if (!customer) unauthorized();
                  const { id } = await params;
                  const order = await getOrder(id);
                  if (!order) notFound();
                  if (order.customer !== customer) forbidden();
                  return (
                    <main>
                      <h1>Order {order.id}</h1>
                      <p>
                        {order.boxes} boxes of {order.tea}
                      </p>
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              4. Question 4 of 7What does a Next.js 16.4 project need before it can call forbidden() or unauthorized()?

                Choose an answer to see the explanation.

              5. Question 5 of 7The order page has a loading.tsx, and nobody is signed in. Which status did /orders/1042 answer with?

                Choose an answer to see the explanation.

              6. Question 6 of 7There's no sale, so this page calls notFound() during next build. Which status does /sale answer with?

                Read the code, then predictTSX
                import { notFound } from "next/navigation";
                import { getSale } from "../_data/sale";
                
                export default async function SalePage() {
                  const sale = await getSale();
                  if (!sale) notFound();
                  return (
                    <main>
                      <h1>{sale.title}</h1>
                      <p>{sale.discount}% off every tea</p>
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              7. Question 7 of 7Why does Next.js add <meta name="robots" content="noindex"> to a not-found page that answered 200?

                Choose an answer to see the explanation.

              Key takeaways

              • notFound() stops a page whose thing doesn't exist, and Next.js shows the nearest not-found.tsx, inside the layouts. It throws, so nothing after it runs.
              • Without a not-found.tsx in the folder, the next one up shows; app/not-found.tsx also answers addresses with no route. With none, Next.js uses its own.
              • unauthorized() means sign in first (401); forbidden() means signed in, and still not allowed (403). They show unauthorized.tsx and forbidden.tsx.
              • In Next.js 16.4.3, forbidden() and unauthorized() are experimental: turn on experimental.authInterrupts in next.config.ts.
              • The status code goes out with the first part of the response. A check during the build gets the real status; a check after the first part was streamed keeps 200 and gets a noindex tag.

              Remember the one-liner.
              Missing, sign in, or not yours: notFound(), unauthorized(), forbidden(), and the status is only real if nothing was sent yet.

              Coming next: Redirects. It shows every way to send a visitor somewhere else, with redirect(), in next.config.ts and in proxy.ts.

              CompleteFrontend Clear concepts. Working examples.