Skip to content

Pages, Layouts, and Nested Routing

In the App Router a folder is a route segment, page.tsx makes that segment publicly routable, and layout.tsx wraps the segment plus every child and stays mounted across navigation.

Each folder under app/ maps to a URL segment. A segment only becomes a reachable URL when it contains a page.tsx.

app/
layout.tsx // root layout (required)
page.tsx // route: /
dashboard/
layout.tsx // wraps /dashboard and its children
page.tsx // route: /dashboard
settings/
page.tsx // route: /dashboard/settings

A page.tsx is a Server Component by default and renders the unique UI for its segment.

app/dashboard/page.tsx
export default function DashboardPage() {
return <h1>Dashboard</h1>
}

Every App Router app needs one root layout. It is the only layout that renders the <html> and <body> tags, and it must render children.

// app/layout.tsx — the required root layout
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

A layout.tsx wraps its own segment and all descendants. Nested layouts compose from the outside in: root wraps dashboard wraps settings. Crucially a layout PERSISTS across navigation — when you move between sibling routes the shared layout does not re-render, so its state and scroll position are preserved.

// app/dashboard/layout.tsx — persists across navigation
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<section>
<nav>{/* stays mounted, keeps scroll + local state */}</nav>
<main>{children}</main>
</section>
)
}

Add loading.tsx and Next.js automatically wraps the segment in a Suspense boundary — the fallback shows instantly while the server work streams in.

// app/dashboard/loading.tsx — automatic Suspense boundary
export default function Loading() {
return <p>Loading dashboard…</p>
}

Add error.tsx and Next.js wraps the segment in an error boundary. It must be a Client Component. As of Next.js 16.3.6 it receives both a stable retry() function (re-fetches and re-renders the segment’s children — the officially recommended one since v16.3.0) and the older reset() (clears the boundary’s error state without repeating the failed fetch). AI-generated snippets trained on older examples almost always write reset only; both still work, but retry is the one Next.js’s own docs now lead with.

app/dashboard/error.tsx
'use client' // error boundaries must be Client Components
export default function Error({
error,
retry,
}: {
error: Error & { digest?: string }
retry: () => void
}) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={() => retry()}>Try again</button>
</div>
)
}

The error boundary hierarchy: a layout’s own error escapes upward

Section titled “The error boundary hierarchy: a layout’s own error escapes upward”

error.tsx wraps loading.tsx, not-found.tsx, page.tsx, and any nested layout.tsx inside the same segment — but it does not wrap the layout.tsx (or template.tsx) that lives in that same folder. If a layout’s own render throws, the error boundary sitting right next to it never sees it; the error keeps unwinding until it reaches the parent segment’s error.tsx.

// app/settings/layout.tsx — this layout's own render throws
export default function SettingsLayout({
children,
}: {
children: React.ReactNode
}) {
throw new Error('layout boom')
return <section>{children}</section>
}
// app/settings/error.tsx — same segment as the layout above: does NOT catch it
'use client'
export default function SettingsError({
error,
retry,
}: {
error: Error & { digest?: string }
retry: () => void
}) {
return (
<div>
<h2>SETTINGS ERROR (same segment)</h2>
<button onClick={() => retry()}>retry</button>
</div>
)
}
// app/error.tsx — the PARENT segment's boundary: this is the one that actually catches it
'use client'
export default function RootError({
error,
retry,
}: {
error: Error & { digest?: string }
retry: () => void
}) {
return (
<div>
<h2>ROOT ERROR BOUNDARY</h2>
<button onClick={() => retry()}>retry</button>
</div>
)
}

Built and started for real (next build && next start), then loaded in a browser, the page’s rendered text proves which boundary actually fired:

document.body.innerText ->
"root-nav
ROOT ERROR BOUNDARY
Minified React error #441 …
retry"

SETTINGS ERROR (same segment) never appears — the layout that threw and the error boundary that would have wrapped it are siblings in the same folder, and error.tsx only wraps what’s below it, not the layout it sits next to.

global-error.tsx replaces the root layout entirely

Section titled “global-error.tsx replaces the root layout entirely”

The same rule applies one level higher, with a twist: the root layout.tsx has no parent segment of its own, so nothing wraps it — not even the root’s own app/error.tsx. If the root layout itself throws, the only thing that can catch it is global-error.tsx, and because it replaces the root layout, it must render its own <html> and <body> tags.

app/global-error.tsx
'use client' // error boundaries must be Client Components
export default function GlobalError({
error,
retry,
}: {
error: Error & { digest?: string }
retry: () => void
}) {
return (
<html>
<body>
<h2>Something went seriously wrong</h2>
<button onClick={() => retry()}>Try again</button>
</body>
</html>
)
}

Proven the same way: with the root layout itself throwing, app/error.tsx’s text never renders — only global-error.tsx’s does:

document.body.innerText ->
"GLOBAL ERROR (root layout itself crashed)
retry"
graph TD
  RL["app/layout.tsx (root layout)"] -->|"throws"| GE["global-error.tsx (only thing above root)"]
  SL["app/settings/layout.tsx (throws)"] -->|"NOT caught by"| SE["app/settings/error.tsx (same segment)"]
  SL -->|"caught by"| RE["app/error.tsx (parent segment)"]
  RE -->|"wraps"| SP["app/settings/page.tsx"]
Which boundary actually catches which error

Read error.tsx as “the boundary for everything inside this folder” — not “the boundary for this folder’s own layout.” A layout and its same-folder error.tsx are siblings, not parent/child, so the layout’s own crash always has to travel up to whichever ancestor segment actually wraps it. The root layout is the extreme case: it has no ancestor at all inside the App Router tree, so only global-error.tsx — which stands entirely outside that tree and rebuilds <html>/<body> itself — can catch it.

FileWhat it wrapsPersists across navigation?
layout.tsxIts own segment + all descendantsYes — does not re-render on sibling navigation
loading.tsxWraps the segment in a Suspense boundaryFallback shows per-navigation, not persistent
error.tsx (same segment)loading/not-found/page/nested layout filesDoes not wrap the layout it sits beside
error.tsx (parent segment)Catches a child layout’s own render errorThe only same-segment boundary at the root layout has no parent — hence global-error.tsx
  • “error.tsx catches any error in its own folder, including the layout.” Proven false above: a settings/layout.tsx crash is invisible to settings/error.tsx and is only caught by the parent segment’s boundary.
  • “reset() is the only prop error.tsx gets.” As of v16.3.0, retry() is the stable, recommended prop — it re-fetches and re-renders, where reset() only clears the boundary’s local error state.
  • “The root layout is protected by app/error.tsx like everything else.” It isn’t — the root layout is the one place in the tree with no parent segment, so its own errors only reach global-error.tsx.

When a generated snippet adds loading.tsx/error.tsx/global-error.tsx, check for these before trusting it:

  • Is error.tsx missing 'use client'? It must be a Client Component — a Server Component version fails at build/runtime, not just lint.
  • Does the snippet assume error.tsx will catch a crash coming from the layout in the same folder? It won’t — that error escapes to the parent segment.
  • Is global-error.tsx missing its own <html>/<body> tags? It replaces the root layout entirely, so without them the page renders no document shell at all.
  • Does the code destructure reset only, with no mention of retry? Not wrong, but it’s using the older API — retry() is what Next.js’s own current docs lead with.
  • Is loading.tsx doing real async work itself (instead of just rendering a fallback)? The automatic Suspense boundary is what streams in the segment below it — loading.tsx itself should stay a synchronous, instant fallback.

🐛 Spot the bug

app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const stats = fetchDashboardStats() // throws if the API is down
return (
<section>
<Header stats={stats} />
{children}
</section>
)
}
// app/dashboard/error.tsx
'use client'
export default function DashboardError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return <button onClick={() => reset()}>Retry loading the dashboard</button>
}
Show the answer

The author put the error-prone data fetch directly in layout.tsx and assumed the error.tsx sitting in the same dashboard/ folder would catch a failure from it. It won’t: error.tsx wraps page.tsx, loading.tsx, not-found.tsx, and any nested layout underneath it, but not the layout it shares a folder with. Proven above with a real next build && next start: the crash surfaces at the parent segment’s boundary instead — in an app where dashboard/ sits directly under the root, that means app/error.tsx (or, for the root layout itself, global-error.tsx). The fix is either to move the data fetch into page.tsx (so a real child boundary wraps it), or to explicitly design for the parent boundary catching it.

What makes a folder segment publicly routable?
What is special about a layout across navigation?
A real app/settings/error.tsx also exists in the same folder. Built and requested for real, which boundary's text renders?
// app/settings/layout.tsx
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
  throw new Error('boom')
  return <section>{children}</section>
}
What must error.tsx be, and what does it receive as of Next.js 16.3.6?
Why must global-error.tsx render its own <html> and <body> tags?
With app/error.tsx AND app/global-error.tsx both present, which one's text actually renders?
// app/layout.tsx (root)
export default function RootLayout({ children }) {
  throw new Error('root boom')
  return <html><body>{children}</body></html>
}