Linking, Navigation, Metadata, and Error Boundaries
Links, navigation, metadata, and error boundaries turn an App Router file tree into a usable application. In this lesson you will build routes that move without full page reloads, describe each route to browsers and crawlers, and contain rendering failures inside the smallest useful segment. The practical outcome is being able to predict what happens when a user clicks a link, when a page changes the document title, when a redirect interrupts rendering, and when a component throws during render.
This chapter belongs early in the Next.js course because later data fetching, authentication, forms, and streaming all rely on predictable route boundaries.
How App Router Navigation Works
The App Router maps folders under app to route segments. A segment can provide page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx, and metadata exports. During navigation, Next.js keeps shared parent layouts mounted, fetches the React Server Component payload for the destination, and patches only the route tree portions that changed. Moving between sibling pages can therefore preserve a sidebar, search box, or media player in a parent layout.
next/link renders an anchor element, so normal browser behaviors such as opening in a new tab still work. For internal routes, the client router intercepts the click, requests the next route payload, and updates the URL and UI without a document reload. Links in the viewport may be prefetched in production. Imperative navigation through useRouter is reserved for actions that happen after code runs, such as moving to a detail page after a successful client-side mutation.
Metadata is computed from the route segment tree. A route can export a static metadata object or an async generateMetadata function. Parent and child metadata are merged, with child values overriding or extending the parent depending on the field. Error boundaries are segment-scoped too. An error.tsx file must be a Client Component because it receives a reset callback. A thrown error in a child segment renders the nearest matching error boundary while preserving layouts above it.
API Anatomy
| Feature | Where it lives | What it controls |
|---|---|---|
<Link href="/courses"> |
Client or Server Component | Declarative internal navigation and optional prefetching |
useRouter() |
Client Component | Imperative push, replace, refresh, and history movement |
metadata |
Page or layout module | Static title, description, robots, alternates, and social metadata |
generateMetadata |
Page or layout module | Parameter-aware metadata generation |
error.tsx |
Route segment | Fallback UI for render errors below that segment |
notFound() or redirect() |
Server rendering path | Stops rendering and selects a missing-resource UI or destination route |
Example 1: Declarative Course Links
Start with links in a Server Component. This component does not need its own client JavaScript; Next.js can still enhance the rendered anchors for client-side transitions.
import Link from "next/link";
const courses = [
{ slug: "nextjs", title: "Next.js Full-Stack Development" },
{ slug: "react", title: "React Foundations" }
];
export default function CoursesPage() {
return (
<main>
<h1>Courses</h1>
<ul>
{courses.map((course) => (
<li key={course.slug}>
<Link href={`/courses/${course.slug}`}>{course.title}</Link>
</li>
))}
</ul>
</main>
);
}
The deterministic output is an unordered list with two anchors: /courses/nextjs and /courses/react. A normal click stays inside the app shell when the target is an internal route. Automatic prefetching improves common destinations, but on very large link lists prefetch={false} can avoid unnecessary requests.
Example 2: Imperative Navigation After a Choice
Use imperative navigation only inside a Client Component. Here a mode selector updates the route without leaving a stale history entry, so Back returns to the previous page instead of each intermediate selection.
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function CatalogModeSwitcher() {
const router = useRouter();
const [mode, setMode] = useState("grid");
function choose(nextMode: string) {
setMode(nextMode);
router.replace(`/catalog?view=${encodeURIComponent(nextMode)}`);
}
return (
<div>
<button aria-pressed={mode === "grid"} onClick={() => choose("grid")}>Grid</button>
<button aria-pressed={mode === "list"} onClick={() => choose("list")}>List</button>
</div>
);
}
Clicking List sets aria-pressed on the List button and changes the URL to /catalog?view=list. The query value is encoded before entering the URL. Router methods must not receive untrusted strings such as javascript: URLs; use validated anchors for external destinations.
Example 3: Metadata for a Dynamic Lesson Page
Dynamic pages often need titles and descriptions derived from route parameters. The metadata function runs on the server side of the route and can reuse the same lookup rules as the page.
import type { Metadata } from "next";
import { notFound } from "next/navigation";
const lessons: Record<string, { title: string; summary: string }> = {
routing: {
title: "Routing Internals",
summary: "Learn how App Router segments become layouts and pages."
}
};
export function generateMetadata({ params }: { params: { slug: string } }): Metadata {
const lesson = lessons[params.slug];
if (!lesson) {
return { title: "Lesson not found" };
}
return {
title: `${lesson.title} | Course Agent`,
description: lesson.summary
};
}
export default function LessonPage({ params }: { params: { slug: string } }) {
const lesson = lessons[params.slug];
if (!lesson) notFound();
return <article><h1>{lesson.title}</h1><p>{lesson.summary}</p></article>;
}
For /lessons/routing, the heading is Routing Internals, the document title is Routing Internals | Course Agent, and the description matches the summary. For an unknown slug, notFound() stops rendering and selects the nearest not-found UI.
Example 4: Segment Error Boundary With Retry
A segment error boundary handles exceptions thrown while rendering its child segment. It does not catch event handler errors or replace monitoring. Its job is to keep the user inside a valid route shell and offer a retry when the failure may be transient.
"use client";
export default function LessonError({
error,
reset
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<section role="alert">
<h2>The lesson could not be loaded</h2>
<p>Try again. If the problem continues, reference code {error.digest ?? "unavailable"}.</p>
<button onClick={reset}>Retry</button>
</section>
);
}
If a child page throws during render, users see the alert section instead of a blank screen. Pressing Retry calls reset, which asks React and Next.js to attempt rendering that segment again. Do not print raw error messages; they can expose file paths, query details, or service names.
Design Choices and Trade-Offs
Choose Link for navigation the user can see and activate. Choose useRouter for navigation caused by program logic after a user action has been handled. Choose redirect() on the server when the requested page should not render for the current state, such as an unauthenticated account page. Choose notFound() when the route shape is valid but the resource does not exist.
Static metadata is fast and easy to cache. Dynamic metadata is more accurate for content pages, but it can add server work before the route is complete. Keep metadata lookups bounded, and avoid duplicating expensive calls between generateMetadata and the page. Segment-level error boundaries provide local recovery, while a root boundary provides a final fallback.
Failure Modes and Troubleshooting
- Symptom: clicking a link reloads the whole document. Cause: the destination is external, malformed, outside the app, or rendered as a plain anchor. Diagnose: inspect the
href, watch the Network panel for a document request, and confirm the route exists underapp. Correct: useLinkfor valid internal routes and anchors for external URLs. - Symptom:
useRouterthrows or the build says it can only be used in a Client Component. Cause: the component is missing"use client". Diagnose: check the first statement of the file and trace imports. Correct: move the interactive control into a small Client Component. - Symptom: the wrong title appears after moving between pages. Cause: parent metadata is being inherited or dynamic metadata returned an incomplete object. Diagnose: inspect the rendered head and compare parent layout metadata with the child export. Correct: set the child
titleanddescriptiondeliberately, or define a title template in the parent. - Symptom: an error boundary never appears. Cause: the error happens in an event handler, middleware, or above the segment where
error.tsxis placed. Diagnose: reproduce with a render-time throw in the target segment. Correct: place the boundary at the risky render path and handle event errors with local state or reporting.
Security, Performance, and Reliability
Navigation APIs affect security because URLs can execute or disclose information if treated casually. Validate any user-controlled destination, prefer internal path construction over string concatenation, and avoid putting secrets or one-time tokens in query strings. Metadata is public; do not include private user data in titles, descriptions, Open Graph fields, or canonical URLs.
Performance depends on small route payloads and shared layouts. Large Client Components around navigation controls force more JavaScript into the browser than needed. Prefetching improves common route transitions but can amplify load when hundreds of links are visible. Error boundaries improve reliability by containing failures and reporting stable identifiers for diagnosis.
Hands-On Lab
Prerequisites: an App Router Next.js project, a terminal, and permission to create routes under app. Use a disposable branch or throwaway project if you are experimenting.
- Create
app/courses/page.tsxwith the first example and visit/courses. - Create
app/catalog/CatalogModeSwitcher.tsxwith the second example, then render it fromapp/catalog/page.tsx. - Create
app/lessons/[slug]/page.tsxwith the metadata example and visit/lessons/routing. - Create
app/lessons/[slug]/error.tsxwith the error boundary. Temporarily addthrow new Error("lab failure")at the top of the lesson page component body. - Remove the temporary throw after verification.
Verification: confirm that course links render as anchors, catalog buttons update ?view= without a full document reload, /lessons/routing has the expected title and heading, an unknown lesson uses not-found UI, and the temporary throw renders the lesson error boundary with a Retry button.
Cleanup: delete the temporary throw, remove lab routes you do not want to keep, and revert experimental metadata titles if they conflict with your real course structure.
Assessment Exercises
- A search result card links to internal lesson pages and external documentation. Which destinations should use
Link, which should use anchors, and why? - A page fetches a lesson by slug in both
generateMetadataand the page component. What risks does that create, and how could you keep behavior consistent? - A settings form calls
router.pushwith a return URL from the query string. Identify the security issue and design a safer approach. - An
error.tsxboundary catches a lesson render failure but not a failed click handler inside a quiz. Explain the difference and propose handling for each failure. - Where would you place error boundaries in a course app with a persistent sidebar, lesson article, and interactive quiz, and what should each boundary preserve?
Summary
App Router navigation is built from route segments, shared layouts, server-rendered payloads, and client-side transitions. Link handles visible internal movement, useRouter handles imperative client transitions, metadata describes each route, and segment error boundaries keep render failures local. Use these tools deliberately so later course features have predictable URLs, public head tags, recoverable failures, and fast navigation.
