Layouts, Templates & Special Files

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Next.js 16.3.8 (the latest release on 2026-10-03, when the facts and examples below were checked; the official documentation reports @doc-version 16.3.8), React 19.3.0, Node.js 20.9.0 or later and TypeScript 5.1 or later. It covers the App Router in depth, because the special files described here are an App Router feature, and it closes with the Pages Router equivalents (app, _document, 404, 500, _error). Caching changes only one thing about these files, how loading.js and a layout that reads request data behave, and the page shows that difference explicitly: every example was built with the previous model (cacheComponents off, the default today) and the one place where Cache Components (cacheComponents: true) behaves differently was built both ways. Everything was built with next build and served with next start in a scratch copy of the _Bookshelf, the catalogue of books that this section grows; state persistence and remounting were observed in a real headless Chromium, and status codes were read with curl.

The page answers four questions: which files does a route segment accept, in which order are they nested, which React boundary does each one create (and therefore what does it catch), and what survives a navigation. The Project Structure page lists the same files as part of the folder layout and shows the minimal hierarchy; here every file gets its own contract, its pitfalls and a worked example. The sibling routing pages of this section (routing fundamentals, dynamic routes and route groups, parallel and intercepting routes) cover the folders that these files live in; they are named in plain prose where they matter. For the React ideas underneath (error boundaries, Suspense, state preservation across renders), see React Routing and Server Rendering, SSG and React Server Components.

The Special Files at a Glance

A route segment (a folder under app) is given UI and behaviour by files with reserved names. Only page (or route) makes the segment public; all the others decorate or guard it. Each file default-exports a React component, and each sits at a fixed place in a fixed nesting order.

Table 1. The file conventions that shape a segment (App Router, Next.js 16.3)
File What it creates Component type and props Introduced

layout.js

UI shared by the segment and everything below it; persists across navigations, keeps state, does not re-render

Server Component by default; children, async params, typed with LayoutProps

13.0

template.js

Like a layout, but with a key so that its children remount on navigation

Server Component by default; children

13.0

loading.js

A React <Suspense> boundary around the page and nested segments, with this file as the fallback

Server Component by default (may be a Client Component); no props

13.0

error.js

A React error boundary around the page and nested segments

Client Component (required); error, retry, reset

13.0

global-error.js

An error boundary that replaces the root layout when it fails

Client Component with its own <html> and <body>; error, retry

13.1

not-found.js

The UI for notFound() and, in the root, for unmatched URLs

Server Component by default; no props

13.0 (root handles unmatched URLs from 13.3)

global-not-found.js

A 404 page that bypasses every layout (experimental flag)

Server Component returning a whole document; no props

15.4

forbidden.js

The UI for forbidden(), with status 403 (experimental flag)

Server Component by default; no props

15.1

unauthorized.js

The UI for unauthorized(), with status 401 (experimental flag)

Server Component by default; no props

15.1

default.js

The fallback of a parallel-route slot (and of children) when its active state cannot be recovered

Server Component; async params

with parallel routes; required for every slot since 16.0

page.js, route.js

The leaf: a UI or an HTTP endpoint that makes the segment public

params and searchParams (page); request and context (route)

13.0 (page), 13.2 (route)

Source: File-system conventions and the version history at the bottom of each page named in the references

The .js in the names stands for any of .js, .jsx and .tsx; the examples use .tsx. The rest of the page takes the files in nesting order, from the outside in.

The Component Hierarchy and the Boundaries It Creates

Inside one segment the files are nested in this order, from the outside in: layout, template, error, loading, not-found (which shares its position with forbidden and unauthorized), and finally page or the layout of the next segment. The components of a segment are nested inside the components of its parent segment. The diagram applies that rule to the route /books/1 of the Bookshelf built later on this page; boxes are boundaries, and a box only protects what is inside it.

flowchart TB GE["global-error.tsx
replaces the root layout when it fails"] subgraph RL["app/layout.tsx - root layout (html, body, header)"] subgraph RN["app/not-found.tsx, forbidden.tsx, unauthorized.tsx
HTTP-access boundary of the root segment"] subgraph BL["app/books/layout.tsx - catalogue layout"] subgraph BT["app/books/template.tsx - key changes with the child segment"] subgraph BE["app/books/error.tsx - error boundary"] subgraph BS["app/books/loading.tsx - Suspense boundary"] subgraph BN["app/books/not-found.tsx - HTTP-access boundary"] subgraph PG["app/books/[id]/page.tsx"] RB["ReviewsBoundary - catchError, component level"] RB --> RV["Reviews component"] end end end end end end end end GE -.-> RL

Reading the diagram from the inside out tells you what each file can catch:

  • An error in Reviews is caught by ReviewsBoundary, the innermost boundary, so the book title stays on screen.

  • An error in page.tsx itself is caught by app/books/error.tsx. The books layout and the template are outside that boundary, so the filter box and the review draft stay where they were.

  • An error thrown by app/books/layout.tsx or app/books/template.tsx is not caught by app/books/error.tsx, because the error boundary is inside them. The next error.js above takes it, and for the root layout that is global-error.js.

  • notFound(), forbidden() and unauthorized() render the nearest file of the matching name above the throwing component. app/books/not-found.tsx sits inside the books layout, so the filter box stays; the root-level forbidden.tsx sits outside it, so the filter box goes with the books layout.

  • The <Suspense> boundary made by loading.js wraps the page and the segments below it, but not the layout or template of its own segment. A slow layout is therefore never covered by that segment’s loading UI.

The flowchart in the worked example later on summarises which file answers each kind of failure.

Root Layout and Nested Layouts

A layout is UI that wraps a segment and everything below it. It default-exports a component that renders its children, which are the next layout down or the page.

The root layout is the topmost layout of the app folder. It is required, and it is the only place where <html> and <body> are written: Next.js does not generate them. The Bookshelf root layout also hosts a header with a client component (a cart counter) that will show why layouts matter:

app/layout.tsx - the root layout of the Bookshelf
import type { Metadata } from "next";
import Link from "next/link";
import { CartBadge } from "@/app/_components/cart-badge";

export const metadata: Metadata = {
  title: { default: "Bookshelf", template: "%s | Bookshelf" },
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  console.log("[render] RootLayout");
  return (
    <html lang="en">
      <body>
        <header>
          <Link href="/">Bookshelf</Link> <Link href="/books">Books</Link>
          <CartBadge />
        </header>
        {children}
      </body>
    </html>
  );
}
app/_components/cart-badge.tsx - client state that lives in the root layout
"use client";

import { useState } from "react";

// Client state that lives in the root layout: it survives every navigation.
export function CartBadge() {
  const [items, setItems] = useState(0);
  return (
    <button onClick={() => setItems((n) => n + 1)} data-testid="cart">
      Cart: {items}
    </button>
  );
}

The rules of the root layout, from the official reference:

  • It must define <html> and <body>.

  • Do not write <head> tags such as <title> and <meta> by hand there; export metadata or generateMetadata, which the Metadata API streams and de-duplicates (the metadata export above sets a title template).

  • It may sit under a dynamic segment, for example app/[locale]/layout.tsx for internationalisation. Dynamic segments above the root layout are root parameters, and next/root-params reads them from any Server Component (shipped in 16.3). The real Bookshelf does exactly this; the scratch app of this page drops the locale to keep the tree small.

  • It cannot call forbidden() or unauthorized().

A nested layout is any layout file below the root. The catalogue layout renders a filter box (client state) next to whatever the child segments render:

app/books/layout.tsx - a nested layout
import { CatalogueFilter } from "./_components/catalogue-filter";

export default function BooksLayout({ children }: { children: React.ReactNode }) {
  console.log("[render] BooksLayout");
  return (
    <section>
      <aside>
        <CatalogueFilter />
      </aside>
      <div>{children}</div>
    </section>
  );
}
app/books/_components/catalogue-filter.tsx - state in a nested layout
"use client";

import { useState } from "react";

// A filter box in the layout: whatever the reader typed stays while they navigate.
export function CatalogueFilter() {
  const [text, setText] = useState("");
  return (
    <input
      aria-label="Filter the catalogue"
      data-testid="filter"
      value={text}
      onChange={(e) => setText(e.target.value)}
    />
  );
}

Props: children, params and the typed helper

A layout receives children (required) and params, a promise of the dynamic parameters from the root segment down to that layout. Await it in an async layout, or read it with React’s use in a Client Component. Reading params synchronously was deprecated in 15 and removed in 16. LayoutProps<'/route'> is a globally available type helper, generated by next dev, next build or next typegen, that types params and also every parallel-route slot of the folder as a named prop:

app/[locale]/layout.tsx - async params and the typed helper
export default async function RootLayout({
  children,
  params,
}: LayoutProps<"/[locale]">) {
  const { locale } = await params;
  return (
    <html lang={locale}>
      <body>{children}</body>
    </html>
  );
}

Layouts persist: state is kept, nothing re-renders

Layouts are not re-rendered on navigation. Moving between pages that share a layout keeps the layout mounted: its client state, its DOM and its focus survive, and the server is not asked to render it again. The scratch run below used the files above. A reader fills in the filter, adds two items to the cart counter, and moves through the catalogue with <Link> (client navigations):

Table 2. State observed after each client navigation (headless Chromium, Next.js 16.3.8)
Navigation Cart (root layout) Filter (books layout) Draft (template) Server log

Load /books, add 2 items, type dune in the filter, type great in the draft

2

dune

great

the layouts render once for the load

Click to /books/1

2

dune

empty (remounted)

no render lines

Click back to /books

2

dune

empty (remounted)

no render lines

Click to /books/2

2

dune

empty (remounted)

no render lines

Click to / (leaves the books layout)

2

the filter is gone (unmounted)

gone

no render lines

The cart counter survives even a navigation out of the catalogue because it lives in the root layout. The console.log lines in the layout files ([render] RootLayout and so on) were added only to count server renders; the log showed nothing during the four navigations. Remove them in real code.

A timeline of three client navigations: /books, /books/1 and /books/2. The row for app/layout.tsx and the row for app/books/layout.tsx are each one continuous green bar, the same instance with its state kept and no server re-render. The row for app/books/template.tsx is three separate orange boxes, mount A with key /books, mount B with key /books/1 and mount C with key /books/2, each with an empty draft. The row for page.tsx is three blue boxes: catalogue, book 1 and book 2.

Source: Layouts and pages ("On navigation, layouts preserve state, remain interactive, and do not rerender")

What a layout cannot do

Because layouts are reused, three things are deliberately unavailable to them, and each has a standard workaround.

Table 3. Layout limits and their workarounds
A layout cannot Why Do this instead

Read searchParams

It would go stale: the layout does not re-render when the query string changes.

Read the page’s searchParams prop, or call useSearchParams in a Client Component inside the layout.

Read the current pathname

Same reason.

Call usePathname in a Client Component.

See the segments below it

A layout only knows its own place in the tree.

Call useSelectedLayoutSegment or useSelectedLayoutSegments in a Client Component (for active links).

Pass data to its children

Data flows through props only, and a layout has no handle on its `children’s props.

Fetch the same data where it is needed: fetch is de-duplicated per request, and React’s cache de-duplicates a database call.

Read the raw request

Next.js keeps user code out of the shared layout so it stays cheap to reuse.

Use cookies() and headers(), which opt the layout into request-time rendering (and, under Cache Components, need a <Suspense> boundary; see the loading.js section).

A navigation-aware link is the usual reason to want the segment. The example below renders the active state without making the layout re-render, because the hook runs in a Client Component:

app/books/_components/nav-link.tsx - active link inside a layout
"use client";

import Link from "next/link";
import { useSelectedLayoutSegment } from "next/navigation";

export function NavLink({ slug, children }: { slug: string; children: React.ReactNode }) {
  const segment = useSelectedLayoutSegment(); // the segment just below this layout
  const active = slug === segment;
  return (
    <Link href={`/books/${slug}`} aria-current={active ? "page" : undefined}>
      {children}
    </Link>
  );
}

Templates and Their Remount Keys

A template wraps a layout’s children just as a layout does, but with one difference that changes everything: each navigation gives the template a new instance. Next.js gives it a key equal to the next segment, so the Client Components inside it lose their state, their effects run again, and their DOM is rebuilt. In the component hierarchy the template sits between the layout and the error boundary; it wraps error, loading, not-found and page, but not the layout of its own segment.

app/books/template.tsx - a template that resets a draft per book
import { ReviewDraft } from "./_components/review-draft";

// Rendered between the layout and the page, with a key that changes with the
// next segment: the draft is reset (a new mount) for every book.
export default function BooksTemplate({ children }: { children: React.ReactNode }) {
  console.log("[render] BooksTemplate");
  return (
    <>
      {children}
      <ReviewDraft />
    </>
  );
}
app/books/_components/review-draft.tsx - state and an effect that reset with the template
"use client";

import { useEffect, useState } from "react";

export function ReviewDraft() {
  const [draft, setDraft] = useState("");

  useEffect(() => {
    // Re-synchronises on every mount, so it runs again after a template remount.
    console.log("review draft mounted");
  }, []);

  return (
    <textarea
      aria-label="Review draft"
      data-testid="draft"
      value={draft}
      onChange={(e) => setDraft(e.target.value)}
    />
  );
}

The behaviour, as the reference states it and as the table above confirmed (the draft emptied on each move between /books, /books/1 and /books/2, and the effect logged "review draft mounted" every time):

  • The key belongs to the template’s own segment level. A template remounts when its child segment, including a dynamic parameter value, changes. Navigations deeper in the tree do not remount a template higher up.

  • Search parameters do not trigger a remount.

  • A template is a Server Component by default, so the examples keep state in a small Client Component inside it.

Use a template when you need to:

  • reset the state of a child Client Component on navigation (a form field, a draft, a filter);

  • re-run an effect on each navigation (page-view tracking, focus management, scroll restoration for the content area);

  • change the framework default for <Suspense>: boundaries placed in a layout show their fallback only on the first load, while those in a template show it on every navigation.

Prefer a layout otherwise; the persistence of layouts is usually what you want. Templates cost a remount per navigation and make Client Component state in them short-lived by design.

The official page walks through a tree with a root template and a blog template: going from / to /about remounts the root template; going from /blog to /blog/first-post leaves the root template alone but remounts the blog template, because the child segment at that level changed.

Source: template.js

loading.js: A Suspense Boundary From a File

loading.js wraps the page and the nested segments in a React <Suspense> boundary and uses its own component as the fallback. While the page’s data streams in, the browser shows the fallback and the shared layout stays interactive. The fallback is prefetched, so the navigation feels immediate, and the navigation is interruptible: a reader can click somewhere else without waiting.

app/books/loading.tsx - the fallback for every page under /books
export default function Loading() {
  return <p data-testid="loading">Loading books...</p>;
}

It is the same boundary you can write by hand around a component with <Suspense fallback={…​}>; use the file for the whole segment and the component for finer parts of the page.

Where the layout is not covered

loading.js sits below the layout, so it cannot cover a layout that waits for request data (cookies(), headers(), an uncached fetch). What happens depends on the caching model:

Table 4. A layout that reads cookies(), built both ways
Model Observed with Next.js 16.3.8 Fix

Previous model (cacheComponents off)

The route is dynamic (ƒ in the route table). The navigation blocks until the layout has finished, and the loading.js fallback is not shown for the layout’s wait.

Move the request-data access into the page, or wrap it in its own <Suspense> boundary in the layout.

Cache Components (cacheComponents: true)

next build fails with "Next.js encountered uncached or runtime data during prerendering" and offers the fixes: wrap in <Suspense>, cache with 'use cache', or set export const instant = false. loading.js is treated as a regular <Suspense> boundary, not as a special prefetch marker.

Wrap the data access in <Suspense>; the route then builds as ◐ Partial Prerender with a static shell.

The fix that worked under both models:

app/dash/layout.tsx - request data behind its own Suspense boundary
import { Suspense } from "react";
import { cookies } from "next/headers";

async function ThemeBadge() {
  const theme = (await cookies()).get("theme")?.value ?? "light";
  return <p>Theme: {theme}</p>;
}

export default function DashLayout({ children }: { children: React.ReactNode }) {
  return (
    <section>
      <Suspense fallback={<p>Theme: ...</p>}>
        <ThemeBadge />
      </Suspense>
      {children}
    </section>
  );
}

Status codes and streaming

When a response streams, the status code is sent with the first bytes, before the page has finished. A loading.js fallback makes the response stream, so notFound(), forbidden() and unauthorized() thrown after that point cannot change the status: it stays 200, and Next.js adds <meta name="robots" content="noindex"> so that crawlers do not index the page. The table in the not-found section shows the effect with real numbers. If a real 404 matters for compliance or analytics, check the resource before streaming starts, for example in proxy.ts, or keep the check before any boundary that suspends.

Source: loading.js

error.js and global-error.js

error.js wraps the segment’s page and nested segments in a React error boundary. When something under it throws while rendering, the boundary shows the file’s component instead. Error boundaries must be Client Components, so the file starts with 'use client'.

app/books/error.tsx - the error boundary of the catalogue
"use client";

export default function BooksError({
  error,
  retry,
}: {
  error: Error & { digest?: string };
  retry: () => void;
}) {
  return (
    <div data-testid="books-error">
      <h2>The catalogue is unavailable</h2>
      {error.digest && <p>Reference: {error.digest}</p>}
      <button onClick={() => retry()}>Try again</button>
    </div>
  );
}

Its props are:

  • error: the error. Errors from Client Components arrive with their original message. Errors from Server Components arrive in production with a generic message and a digest, a hash that you can search for in the server logs, so that internals never reach the browser (in development the message is included).

  • retry: asks Next.js to re-fetch and re-render the boundary’s children, and replaces the fallback when it works. Use this for temporary failures. It is the recommended way to recover.

  • reset: clears the error state and re-renders the children without re-fetching. It cannot fix a failure that happened in a Server Component; use it only when you have a specific reason.

The scratch run with a simulated outage (/books/outage throws in the page) rendered app/books/error.tsx inside the books layout, with the cart, the filter and the draft all intact; the Reference line printed the digest. The HTTP status was 200 because the segment has a loading.tsx (the response was already streaming), and 500 when the same failure occurred on a route without a boundary that streams.

retry is the stable name; unstable_retry is history

Before 16.2 the prop was called reset only. Version 16.2 added unstable_retry and unstable_catchError; version 16.3.0 made them stable as retry and catchError. In 16.3.8 the props object that a segment’s error component receives has exactly three keys: error, reset and retry. The old unstable_ names are only mentioned in the version history of the official pages, so new code uses the stable names and code written against 16.2 should be renamed when upgrading.

global-error.js: the root layout’s safety net

error.js is inside the layout and template of its own segment, so it cannot catch their errors, and the root layout has no error.js above it. app/global-error.js handles that case. It replaces the root layout while it is active, so it must render its own <html> and <body>, and it cannot use metadata exports (use the React <title> component). It does not include your global styles or theme either. Since 15.2 it also appears in development.

app/global-error.tsx - replaces the root layout when it fails
"use client";

export default function GlobalError({ retry }: { error: Error & { digest?: string }; retry: () => void }) {
  return (
    // Replaces the root layout while active, so it needs its own <html> and <body>.
    <html lang="en">
      <body>
        <h1>Bookshelf is having trouble</h1>
        <button onClick={() => retry()}>Try again</button>
      </body>
    </html>
  );
}

To check it, the scratch root layout was made to throw on request (an environment variable read at request time); the server answered 500 and the browser rendered the global-error UI.

catchError: a boundary for part of a page

error.js protects whole segments. For a component that can fail on its own (a reviews widget, an external embed), catchError from next/error builds a boundary component you place anywhere in a Client Component tree. Compared with a hand-written class boundary it re-renders through retry() in a transition (so Client Component state outside the boundary survives), it lets redirect() and notFound() pass through instead of catching them, and its error state clears on a client navigation.

app/books/[id]/_components/reviews-boundary.tsx - a component-level boundary
"use client";

import { catchError, type ErrorInfo } from "next/error";

function ReviewsFallback({ title }: { title: string }, { retry }: ErrorInfo) {
  return (
    <div data-testid="reviews-fallback">
      <h3>{title}</h3>
      <button onClick={() => retry()}>Try again</button>
    </div>
  );
}

// A component-level error boundary: the rest of the page keeps working.
export const ReviewsBoundary = catchError(ReviewsFallback);
app/books/[id]/page.tsx - the page uses the boundary and notFound()
import { notFound } from "next/navigation";
import { getBook } from "@/lib/books";
import { ReviewsBoundary } from "./_components/reviews-boundary";
import { Reviews } from "./_components/reviews";

export default async function BookPage({ params }: PageProps<"/books/[id]">) {
  const { id } = await params;
  if (id === "outage") throw new Error("catalogue database unavailable"); // simulated failure
  const book = await getBook(id);
  if (!book) notFound();

  return (
    <article>
      <h1 data-testid="book">{book.title}</h1>
      <p>{book.author}</p>
      <ReviewsBoundary title="Reviews are unavailable">
        <Reviews bookId={book.id} />
      </ReviewsBoundary>
    </article>
  );
}

On /books/3 (the simulated reviews outage) the page rendered the book title and the boundary’s fallback, the catalogue filter and the cart stayed in place, and the segment’s error.tsx was not involved.

not-found, forbidden and unauthorized

Three files render the UI for an HTTP-style interruption. All three are Server Components by default (they may be async) and take no props. They are created by calling a function that throws, so call it in the render path (a component, or a function that a component awaits) and do not return it.

Table 5. The three interruption files
Function File Status Notes

notFound() from next/navigation

not-found.js

404 (200 when streamed)

Stable. Also serves unmatched URLs when it is the root app/not-found.js.

forbidden() from next/navigation

forbidden.js

403

Experimental in 16.3.8: needs experimental.authInterrupts. Cannot be called in the root layout.

unauthorized() from next/navigation

unauthorized.js

401

Experimental in 16.3.8: needs experimental.authInterrupts. Cannot be called in the root layout.

not-found.js

app/books/not-found.tsx - the not-found UI inside the catalogue layout
import Link from "next/link";

export default function BookNotFound() {
  return (
    <div data-testid="books-nf">
      <h2>We do not have that book</h2>
      <Link href="/books">Back to the catalogue</Link>
    </div>
  );
}
app/not-found.tsx - the root not-found: also the 404 for unmatched URLs
import Link from "next/link";

export default function NotFound() {
  return (
    <main data-testid="nf">
      <h1>Page not found</h1>
      <Link href="/">Go home</Link>
    </main>
  );
}

The root app/not-found.js handles two cases: a thrown notFound() that no deeper file catches, and any URL that no route matches. The deeper file in the example above keeps the catalogue layout around the message. In the scratch run without it, a missing book replaced the whole books layout (the filter box disappeared), because the root boundary sits outside that layout; adding app/books/not-found.tsx kept the layout. Place the file at the level whose chrome you want to keep.

An experimental global-not-found.js (introduced in 15.4, enabled with experimental.globalNotFound) renders a complete document for unmatched URLs and skips all layouts. It exists for apps that have several root layouts or a root layout under a dynamic segment, where no single layout can compose a 404 page; it is covered in the multiple-root-layouts section.

forbidden.js and unauthorized.js

unauthorized() means "this request is not signed in" (401), forbidden() means "signed in, but not allowed" (403). Both render the nearest file of the matching name above the throwing component, with noindex, and both can be called from Server Components, Server Functions and Route Handlers. They are experimental in 16.3.8, so enable them:

next.config.ts - enable the interruption APIs
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: { authInterrupts: true },
};

export default nextConfig;

Without the flag, calling them fails at request time (observed): the server logs "`unauthorized()` is experimental and only allowed to be used when experimental.authInterrupts is enabled" and the response is a 500.

The check itself belongs in a data access layer, not in the page, so that every caller applies the same rule. The scratch version below maps an opaque session id (kept on the server) to a role; the real check, with signed sessions, is the subject of the authentication pages of this section.

lib/dal.ts - a stand-in data access layer (server-only)
import "server-only";
import { cache } from "react";
import { cookies } from "next/headers";

// Stand-in for the real Data Access Layer: maps an opaque session id (kept on the
// server) to a role. A real application validates a signed or stored session.
const sessions = new Map([
  ["demo-reader-session", { userId: "reader-1", role: "reader" as const }],
  ["demo-admin-session", { userId: "admin-1", role: "admin" as const }],
]);

export const getSession = cache(async () => {
  const id = (await cookies()).get("session")?.value;
  return id ? (sessions.get(id) ?? null) : null;
});
app/admin/page.tsx - 401 for anonymous readers, 403 for readers who are not admins
import { forbidden, unauthorized } from "next/navigation";
import { getSession } from "@/lib/dal";

export default async function AdminPage() {
  const session = await getSession();
  if (!session) unauthorized(); // 401: not signed in
  if (session.role !== "admin") forbidden(); // 403: signed in, not allowed

  return <h1>Admin dashboard</h1>;
}
app/forbidden.tsx - the 403 UI
export default function Forbidden() {
  return <main data-testid="forbidden"><h1>You may not see this page</h1></main>;
}
app/unauthorized.tsx - the 401 UI
import Link from "next/link";

export default function Unauthorized() {
  return (
    <main data-testid="unauthorized">
      <h1>Please sign in</h1>
      <Link href="/sign-in">Sign in</Link>
    </main>
  );
}

The interruption files are UI, not security. They change what is shown after the check has run on the server; the protection is the check in the data access layer (and the same check in every Server Function and Route Handler). Never rely on a layout, a template or a client-side guard to protect data: layouts are not re-rendered on navigation, so a check placed there does not run again.

Status codes, measured

The same curl run against next start gave these results. The right-hand column is the one to remember: where the check happens relative to the first byte decides the status.

Table 6. HTTP status per situation (Next.js 16.3.8, previous caching model)
Request Status Why

/ and /books/1 (found)

200

Normal render.

/nope (no route matches)

404

The root not-found.tsx renders inside the root layout; noindex is added.

/books/99 (notFound() in a page under app/books/loading.tsx)

200

The response was already streaming when the page threw. noindex is added. The books not-found.tsx renders after hydration.

/books/outage (page throws, segment has loading.tsx)

200

Same reason; the error boundary renders in the browser.

/admin without a session

401

unauthorized() ran before any streaming: no loading.tsx above this page.

/admin with a reader session

403

forbidden(), same reason.

/admin with an admin session

200

The page.

A page that throws, in a route without loading.tsx

500

The failure happens before the first byte, so the status can still change.

A layout that throws (app/oops/layout.tsx in a side experiment)

500

Caught client-side by the root error.tsx, with the root layout still on screen.

The root layout throws

500

global-error.tsx renders.

The experiment with a plain page and no loading.tsx confirmed the 404, 401 and 403 values. The practical rule: put the access check before any await that can suspend, and do not put a loading.js above a page whose status matters to crawlers.

default.js: The Fallback of a Parallel Slot

A parallel route is a named slot (a folder starting with @) that a layout renders as a prop next to children. During a client navigation Next.js remembers which sub-page each slot shows; after a full page load it cannot, and a slot that has no page matching the current URL needs a fallback. That is default.js. Since 16.0 every slot must have one, and children is an implicit slot, so it needs one as well whenever its parent page cannot be recovered; otherwise the build fails, or the route returns 404.

A minimal layout with a stats slot, built to check the behaviour:

The slot folder layout
app/team/
├── layout.tsx            # renders {children} and {stats}
├── page.tsx              # /team
├── settings/page.tsx     # /team/settings (no matching page in @stats)
└── @stats/
    ├── page.tsx          # the slot at /team
    └── default.tsx       # the fallback for every other URL

Without @stats/default.tsx, next build stopped with this error:

next build without the fallback
Error: Missing required default.js file for parallel route at /team/@stats
The parallel route slot "@stats" is missing a default.js file. When using parallel routes, each slot must
have a default.js file to serve as a fallback.
Create a default.js file at: /team/@stats/default.js
https://nextjs.org/docs/messages/slot-missing-default

With the fallback in place:

app/team/@stats/default.tsx
export default function StatsDefault() {
  return <p>stats default</p>;
}

The two navigations then behave differently, which is the point of the file (observed):

Table 7. Slot content after a hard load and after a soft navigation to /team/settings
How you arrive children @stats slot

Load /team/settings directly (hard navigation)

team settings

stats default (from default.tsx)

Click from /team to /team/settings (soft navigation)

team settings

stats overview (the slot keeps its previous active state)

To get the old behaviour of a 404 instead of a fallback, have default.tsx call notFound(). default.js receives an async params like a layout. Intercepting routes and modal patterns use the same mechanism and are covered by the parallel-and-intercepting routes page of this section; the @modal/default.tsx of the full Bookshelf is the same file.

Multiple Root Layouts

Any layout with no layout.js above it is a root layout, and an app may have several. There are two ways to get them: route groups such as app/(shop)/layout.tsx and app/(admin)/layout.tsx (the group names are not in the URL), or no app/layout.js at all so that app/dashboard/layout.js and app/blog/layout.js each become a root. Each root layout writes its own <html> and <body>:

app/(shop)/layout.tsx and app/(admin)/layout.tsx - two roots (each has its own html and body)
import Link from "next/link";

// app/(shop)/layout.tsx
export default function ShopLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <nav><Link href="/">Shop</Link> <Link href="/books">Books</Link> <Link href="/admin">Admin</Link></nav>
        {children}
      </body>
    </html>
  );
}

// app/(admin)/layout.tsx has the same shape with its own chrome:
// <html lang="en"><body className="admin">...{children}</body></html>

Navigating across root layouts is a hard navigation: the browser performs a full page load. Within one root layout, navigation stays client-side. Measured with a marker set on window before the click:

Table 8. Soft and hard navigation between two roots (observed)
Navigation Marker on window

/ to /books (both under the shop root)

kept, so it was a client navigation

/books to /admin (shop root to admin root)

gone, so it was a full page load

Two consequences follow. Put the sections whose chrome differs in different roots only when the full load is acceptable. And an app with several roots has no single layout in which to compose a 404 page; an unmatched URL then shows the framework’s default "404: This page could not be found." page. The experimental global-not-found.js fixes that:

next.config.ts - enable global-not-found
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: { globalNotFound: true },
};

export default nextConfig;
app/global-not-found.tsx - a whole document, no layouts involved
export const metadata = { title: "404 - Bookshelf" };

export default function GlobalNotFound() {
  return (
    <html lang="en">
      <body>
        <h1 data-testid="global-nf">404 - this page does not exist</h1>
      </body>
    </html>
  );
}

With the flag on, /nope answered 404 with the custom <title> and body. The file bypasses rendering, so import any global CSS and fonts it needs itself; it also does not read an app-level theme.

Source: layout.js: root layout, global-not-found.js and the Project Structure page for the folder side of the same decision

Worked Example: The Bookshelf Segment

The files above form one working tree. It is the part of the Bookshelf that this page needs: a root layout with a client cart, a catalogue segment with a layout, a template, a loading UI, an error boundary and a not-found file, a book page with a component-level boundary, and an admin page guarded by unauthorized() and forbidden().

The scratch Bookshelf tree used on this page
app/
├── layout.tsx                    # root layout: html, body, header, CartBadge
├── page.tsx                      # /
├── not-found.tsx                 # 404 for unmatched URLs and uncaught notFound()
├── forbidden.tsx                 # 403 UI
├── unauthorized.tsx              # 401 UI
├── global-error.tsx              # replaces the root layout when it fails
├── _components/cart-badge.tsx
├── admin/page.tsx                # /admin: unauthorized() / forbidden()
└── books/
    ├── layout.tsx                # catalogue layout (filter box)
    ├── template.tsx              # remounts a review draft per book
    ├── loading.tsx               # Suspense fallback
    ├── error.tsx                 # error boundary
    ├── not-found.tsx             # not-found inside the layout
    ├── page.tsx                  # /books
    ├── _components/              # private: catalogue-filter, review-draft
    └── [id]/
        ├── page.tsx              # /books/1
        └── _components/          # private: reviews, reviews-boundary

app/books/page.tsx and the Reviews component complete the picture:

app/books/page.tsx - the catalogue page
import Link from "next/link";
import { listBooks } from "@/lib/books";

export default async function BooksPage() {
  const books = await listBooks();
  return (
    <ul>
      {books.map((book) => (
        <li key={book.id}>
          <Link href={`/books/${book.id}`}>{book.title}</Link>
        </li>
      ))}
    </ul>
  );
}
app/books/[id]/_components/reviews.tsx - throws for book 3 to simulate an outage
import { listReviews } from "@/lib/books";

export async function Reviews({ bookId }: { bookId: string }) {
  if (bookId === "3") throw new Error("reviews service unavailable"); // simulated failure
  const reviews = await listReviews(bookId);
  return <p data-testid="reviews">{reviews.length} reviews</p>;
}

next build listed the routes as a static /books, a dynamic /books/[id] and /admin, and the route / and /_not-found as static:

next build route table (previous model)
Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /admin
├ ○ /books
└ ƒ /books/[id]

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand

The flowchart summarises which file answers each situation in this tree:

flowchart TD start["Something goes wrong while rendering a segment"] --> kind{"What was thrown?"} kind -- "notFound()" --> nf["Nearest not-found.js above the thrower
app/books/not-found.tsx, else app/not-found.tsx"] kind -- "forbidden()" --> fb["Nearest forbidden.js above the thrower
app/forbidden.tsx"] kind -- "unauthorized()" --> ua["Nearest unauthorized.js above the thrower
app/unauthorized.tsx"] kind -- "any other Error" --> where{"Where was it thrown?"} where -- "inside a catchError boundary" --> ce["Its fallback
ReviewsBoundary"] where -- "page or nested segment" --> er["Nearest error.js above the thrower
app/books/error.tsx"] where -- "layout or template of that same segment" --> up["The next error.js above that segment;
none in the Bookshelf, so global-error.tsx"] nf --> st{"Was the response already streaming?"} fb --> st ua --> st st -- "no" --> code["Status 404, 403 or 401"] st -- "yes, loading.js sent a fallback" --> ok["Status 200 plus noindex"]

Putting it together, the observed behaviour of the whole tree (every row from a real next start run):

Table 9. Behaviour of the worked example
Action Result

Click Cart twice in the header, then move through /books, /books/1, /books/2 and /

The count stays 2 everywhere: the root layout never remounts.

Type in the catalogue filter, then move between book pages

The text stays: the books layout persists while the URL is under /books.

Type in the draft box, then open another book

The draft empties: the template remounts for each child segment.

Open /books/3

The book title and the "Reviews are unavailable" fallback; filter and cart untouched.

Open /books/outage

app/books/error.tsx inside the layout, with a digest reference.

Open /books/99

app/books/not-found.tsx inside the layout (status 200, noindex).

Open /nope

app/not-found.tsx inside the root layout, status 404.

Open /admin with no cookie, with a reader cookie, with an admin cookie

401, 403 and the dashboard (200).

The Pages Router Equivalents

The Pages Router has no nested layouts, no per-segment boundaries and no template, but it has equivalents for the important pieces. This is the same Bookshelf shell in the pages folder (verified with next build and next start in its own scratch project).

pages/_app.tsx - a shared layout that keeps state across pages
import type { AppProps } from "next/app";
import Link from "next/link";
import { useState } from "react";

export default function MyApp({ Component, pageProps }: AppProps) {
  const [items, setItems] = useState(0);
  return (
    <>
      <header>
        <Link href="/">Bookshelf</Link> <Link href="/books">Books</Link>
        <button onClick={() => setItems((n) => n + 1)}>Cart: {items}</button>
      </header>
      <Component {...pageProps} />
    </>
  );
}
pages/_document.tsx - the html and body (server only)
import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="en">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}
pages/404.tsx and pages/500.tsx - static error pages
// pages/404.tsx
export default function Custom404() {
  return <h1>404 - Page Not Found</h1>;
}

// pages/500.tsx
export default function Custom500() {
  return <h1>500 - Server-side error occurred</h1>;
}
pages/_error.tsx - the general error page (used when no 404.tsx or 500.tsx matches)
import type { NextPageContext } from "next";

function CustomError({ statusCode }: { statusCode?: number }) {
  return (
    <p>{statusCode ? `An error ${statusCode} occurred on server` : "An error occurred on client"}</p>
  );
}

CustomError.getInitialProps = ({ res, err }: NextPageContext) => {
  const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
  return { statusCode };
};

export default CustomError;

In the scratch project: after two clicks on the cart and a client navigation from / to /books the count was still 2 (_app persists), /nope and a page returning { notFound: true } from getServerSideProps both answered 404 with the custom 404 page, and an error thrown in getServerSideProps or during rendering answered 500 with pages/500.tsx. Because 500.tsx existed, _error.tsx was not used for those errors; it is the catch-all for other status codes. The build lists /404 and /500 as static pages and /_app without a route.

Table 10. App Router files and their Pages Router counterparts
Concern App Router Pages Router

Shared UI that persists

layout.js, nested per segment

_app.js wraps every page; per-page nesting with a getLayout property on the page component, read in _app

<html> and <body>

The root layout

_document.js (server only, no event handlers)

Reset state per navigation

template.js

No file; give <Component> a React key

Loading UI

loading.js (Suspense)

No file convention; client-side state or router events

Error boundary per segment

error.js

None built in; write a React error boundary in _app

Last-resort error

global-error.js

_error.js (production only) and static 500.js

404

not-found.js, notFound(), global-not-found.js

404.js; { notFound: true } from getStaticProps or getServerSideProps

401 and 403

unauthorized.js and forbidden.js (experimental)

None; set the status or redirect yourself

Parallel slot fallback

default.js

Not applicable

getInitialProps in _app disables Automatic Static Optimization for pages without getStaticProps, which is why the official page discourages it; the introduction page compares the two routers as a whole and explains how to migrate one route at a time.

What Changed Since Next.js 13 and 14

The files of this page appeared with the App Router in 13.0 and have been refined since. These are the changes you will meet when reading older code or books.

Table 11. Changes to the special files, by version
Version Change

13.0

layout, template, loading, error, not-found and default introduced with the App Router.

13.1

global-error introduced.

13.3

The root app/not-found also handles unmatched URLs.

15.0

params in layouts, pages and default is a Promise (a codemod exists); synchronous access still worked in 15, but was announced as deprecated.

15.1

forbidden.js and unauthorized.js introduced, behind experimental.authInterrupts.

15.2

global-error also renders in development.

15.4

global-not-found.js introduced (experimental).

16.0

Synchronous access to params removed; every parallel-route slot needs an explicit default.js.

16.2

unstable_retry (error pages) and unstable_catchError added.

16.3

retry and catchError stable. next/root-params ships for reading root parameters under a dynamic root layout.

Version tags are from the official version-history tables of each page and from the upgrade guides; the 16.0 default.js requirement was also reproduced in the scratch build error shown earlier. forbidden and unauthorized were still labelled experimental in the 16.3.8 documentation and still require the authInterrupts flag.

Where the Books Are Outdated

The two consulted books describe Next.js 12 and 13 era projects, mostly built on the Pages Router. For the topics of this page:

Table 12. Statements to unlearn
What the books show or imply Current approach

One _app.js as the only shared layout, and getLayout for anything nested

The App Router has native nested layouts; getLayout is only for the Pages Router.

_document.js for <html>, <body> and global tags

In the App Router the root layout writes <html> and <body>, and the Metadata API replaces hand-written <head> tags and next/head.

pages/_error.js and pages/404.js as the whole error story

Per-segment error.js, not-found.js and global-error.js, plus catchError for components. _error.js and 404.js remain only for the Pages Router.

An error.js that receives reset only

retry re-fetches and re-renders; reset only clears the state. (unstable_retry existed in 16.2 only.)

Reading props.params.id synchronously in a layout or page

params is a Promise since 15, and synchronous access was removed in 16: await params or use(params).

Authorization by redirect in a client hook, or only by useSession

Check on the server in the data access layer; unauthorized() and forbidden() render the 401 and 403 UI. Client hooks only improve the UI.

Treating every non-200 response as a failure of the page

With streaming, notFound() after the first byte stays 200 plus noindex; do the check before the first await that suspends if the status matters.

Common Pitfalls

Table 13. Pitfalls with layouts, templates and special files
Symptom Cause Fix

A layout does not update when the query string or the URL changes

Layouts do not re-render on navigation.

Read searchParams in the page, or useSearchParams and usePathname in a Client Component.

State survives a navigation when you wanted it reset

The component lives in a layout.

Move it to a template.js, or give it a key that changes.

State is lost on every navigation when you wanted it kept

The component lives in a template (or sits in a page).

Move it to a layout.

error.js does not catch an error from the layout next to it

The boundary is inside the layout and the template of its segment.

Put the error.js one level up, or use global-error.js for the root layout.

global-error.js renders a page without styles, or fails to render

It replaces the root layout and must provide <html> and <body>; global styles are not inherited.

Add the document tags and import what it needs; use the React <title> instead of metadata.

next build fails with "app/x/error.tsx must be a Client Component"

Error boundaries must be Client Components.

Add 'use client' at the top of the file.

The "Try again" button does nothing for a Server Component failure

reset() only clears the client error state.

Call retry().

The whole layout disappears when a page calls notFound()

The nearest not-found.js is the root one, outside the layout.

Add a not-found.js in the segment whose layout you want to keep (likewise for forbidden.js and unauthorized.js).

The page returns 200 for a missing resource

A loading.js or Suspense boundary made the response stream before notFound() ran.

Check before the first suspending await, or check in proxy.ts. The noindex tag is still emitted.

forbidden() or unauthorized() returns a 500 and a log message about authInterrupts

The experimental flag is off.

Set experimental.authInterrupts to true in next.config.ts.

forbidden() or unauthorized() seems to do nothing

It was called in an un-awaited promise, or inside a try/catch that swallowed the thrown error.

Always await the function that may call it; do not catch its error; the root layout cannot call them.

next build fails with "Missing required default.js file for parallel route"

Since 16.0 every slot needs one.

Add default.js (call notFound() in it for the old 404 behaviour).

next build fails with "Next.js encountered uncached or runtime data during prerendering" in a layout

Cache Components: cookies(), headers() or an uncached fetch outside <Suspense>.

Wrap the access in <Suspense>, cache it with 'use cache', or set export const instant = false.

A click between two sections reloads the whole page

The sections use different root layouts.

Expected; move the sections under one root layout if client navigation matters.

<title> or <meta> written by hand in the root layout is duplicated or ignored

The root layout must not render <head> tags manually.

Export metadata or generateMetadata.

Layout data cannot be passed to the page

Layouts cannot pass props to their children.

Fetch in both places; fetch and React cache de-duplicate within a request.