Verification tools
Next.js has no in-browser playground — the only way to prove a claim about it is a real project, a real tsc/next build run, or a real browser. Every output below is real, captured against Next.js 16.3.6 and React 19.2.8. None of these tools overlap perfectly, and none is close to complete on its own — the AI Bug Catalog’s “Caught by” line for each of its 25 entries names exactly which one (if any) of the tools below would have caught it.
tsc --noEmit — the fast, first pass
Section titled “tsc --noEmit — the fast, first pass”Type-checking alone catches surprisingly little of this catalog by design — most of Next.js’s own rules (Server/Client boundaries, cacheComponents conflicts, prerender-time data access) are enforced by the framework’s build step, not by TypeScript’s type system. What tsc does catch reliably: a genuine signature change like revalidateTag now requiring two arguments, and any params/searchParams access that skips await — but only when the prop is actually typed as a Promise (a hand-written, loose object type defeats this silently; the generated PageProps/LayoutProps helpers, from next typegen, always get it right).
npx tsc --noEmitapp/actions.ts(6,3): error TS2554: Expected 2 arguments, but got 1.Reading next build output
Section titled “Reading next build output”next build prints a route table after every successful build, one symbol per route:
The full legend (official docs, verbatim):
○ (Static) Fully prerendered at build time. Served without server rendering.◐ (Partial Prerender) Static shell served immediately. Dynamic content streams in at request time.● (SSG) Prerendered static HTML (from generateStaticParams or getStaticProps).ƒ (Dynamic) Server-rendered on demand for each request.○ Static, ● SSG, and ƒ Dynamic are the modes Next.js has always rendered in, App Router or Pages Router — real next build output, default config, no cacheComponents, a page reading cookies():
Route (app)┌ ○ /└ ƒ /catalog/11-cookies-static◐ Partial Prerender only appears once Cache Components is on (cacheComponents: true) — real output from the bundled next build guide, adding generateStaticParams to a dynamic product page:
Route (app)┌ ○ /├ ○ /_not-found└ /products/[id] ├ ◐ /products/[id] # Params known, data still uncached ├ ◐ /products/1 ├ ◐ /products/2 └ ◐ /products/3Without Cache Components, reading a route’s cache behavior means checking cookies()/headers()/fetch calls by eye — the table itself just prints ƒ the moment any of them run, silently, as the AI Bug Catalog’s entry 11 shows.
The warnings worth reading past the route table, not just the pass/fail exit code:
⚠ The Edge Runtime is deprecated. You can use the "nodejs" runtime instead. Learn more: https://nextjs.org/docs/messages/edge-runtime-deprecated⚠ metadataBase property in metadata export is not set for resolving social open graph or twitter images, using "http://localhost:3000".⚠ The "middleware" file convention is deprecated. Please use "proxy" instead.None of these three fail the build or the CI exit code — all three still print next build’s normal success output and exit 0. A CI pipeline that only checks the exit code misses all three.
What replaced next lint
Section titled “What replaced next lint”Starting with Next.js 16, the next lint command itself is removed — next build no longer runs linting as a build step at all. The bundled docs are explicit about the replacement:
The
next lintcommand has been removed. Use Biome or ESLint directly.
For ESLint specifically, eslint-config-next still ships the same @next/eslint-plugin-next rules (including no-html-link-for-pages, no-img-element, and the rest) — you now run the ESLint CLI yourself, against a flat eslint.config.mjs:
npx eslint .A codemod migrates an existing next lint setup automatically:
npx @next/codemod@canary next-lint-to-eslint-cli .next experimental-analyze — real bundle composition
Section titled “next experimental-analyze — real bundle composition”Since next build stopped printing a per-route bundle-size column (v16.0.0), next experimental-analyze (stable as of v16.1.0) is the tool that shows what actually shipped to the browser, filterable by route and by client/server:
npx next experimental-analyze --output✓ Running next.config.ts took 19ms Analyzing a production build...✓ Analyze completed in 926ms. Results written to .next/diagnostics/analyze.To explore the analyze results interactively, run `next experimental-analyze` without `--output`.Without --output, it starts a local server with an interactive treemap instead of writing static files — useful for the exact kind of comparison the AI Bug Catalog’s entry 1 makes (an 8,967-byte chunk versus a 287-byte one, depending on where 'use client' sits).
next typegen — route types without a full build
Section titled “next typegen — route types without a full build”next typegen generates the PageProps/LayoutProps/RouteContext helper types (and next-env.d.ts) without running a full next dev or next build — useful in CI, where you want type-checking without paying for a full build:
npx next typegenGenerating route types...✓ Types generated successfullyRun it before tsc --noEmit in a CI job that only wants to type-check: next typegen && tsc --noEmit. Without it (or without next dev/next build having run at least once), the generated PageProps<'/route'> helper types simply do not exist yet.
next build --debug-prerender — real stack traces for prerender errors
Section titled “next build --debug-prerender — real stack traces for prerender errors”Production builds minify server code and skip source maps by default, so a Cache-Components prerender error’s stack trace is close to useless on its own. --debug-prerender (stable since v15.4.0) disables minification, turns on server-side source maps, and — critically — continues past the first prerender failure so every blocked route shows up in one run instead of one per rebuild:
npx next build --debug-prerenderA blocking-prerender error with --debug-prerender points directly at the offending line (real output, from the bundled next build guide, for an unwrapped params read):
Error: Route "/products/[id]": Next.js encountered uncached or runtime data during prerendering. ... at Page (app/products/[id]/page.tsx:2:30) 1 | export default async function Page(props: PageProps<'/products/[id]'>) {> 2 | const { id } = await props.params | ^ 3 | const res = await fetch(`https://api.example.com/products/${id}`)Warning, straight from the CLI: never deploy a build produced with --debug-prerender — it skips optimizations production needs.
Reading the RSC payload in the Network tab
Section titled “Reading the RSC payload in the Network tab”Every navigation and every initial page load carries a React Server Components payload — a compact, streaming, non-JSON serialization of the server-rendered tree — visible in the browser’s Network tab as the page’s own document request (for the initial load) or as a text/x-component fetch (for a client-side navigation). A real payload fragment looks like this (trimmed, from an actual next build + next start run):
0:{"P":null,"c":["","bug-public-env"],"q":"","i":false,"f":[[["",{"children":["bug-public-env",{"children":["__PAGE__",{},"$undefined","$undefined",4608]},"$undefined","$undefined",4608]},"$undefined","$undefined",4624], ...You will not read this format character by character in practice — the point of loading it in the Network tab is confirming that a navigation triggered a real RSC fetch at all (versus a full page reload from a stray <a>, per the AI Bug Catalog’s entry 25), and roughly how large it is relative to the actual content on screen.
A Playwright smoke test
Section titled “A Playwright smoke test”A single Playwright test that loads the real, built app and asserts on the final DOM state catches an entire class of bug this page’s static tools cannot: anything that only manifests after hydration, after a client-side navigation, or after a Server Action round-trip. This course’s own probe used exactly this technique to prove the NEXT_PUBLIC_ dynamic-access bug in the AI Bug Catalog’s entry 19 — the static-literal read was correct in the initial server-rendered HTML, and only a real browser, after hydration, exposed the dynamic read’s undefined.
// A Playwright test file (tests/smoke.spec.ts) — illustrative, not a// fence this course's own tsc harness compiles, since @playwright/test// isn't a dependency of the type-check probe.import { test, expect } from '@playwright/test'
test('renders the real client-side value, not the SSR one', async ({ page }) => { await page.goto('http://localhost:3000/some-route') await expect(page.locator('body')).toContainText('value: undefined')})instrumentation.ts in development, not just production
Section titled “instrumentation.ts in development, not just production”register() in instrumentation.ts runs once for every new Next.js server instance — including next dev — not only in a deployed production build. Wiring onRequestError early, even just to console.error during local development, surfaces a server error the moment it happens instead of only after a report reaches production. See Deployment and Self-Hosting for the full, real onRequestError example this course builds against a production observability provider.
This course’s own harness: compile every snippet
Section titled “This course’s own harness: compile every snippet”Every code fence in this course tagged with a // path.ext first line is a real file, collected by tools/verify-snippets.mjs into a namespaced directory per lesson and type-checked in one tsc --noEmit pass across the whole course — the same principle as the Playwright smoke test above, generalized to “does every snippet the reader might copy-paste actually compile,” not just the one file you happen to be editing right now:
node tools/verify-snippets.mjsno type errors.
Per-module fence summary (collected / skipped-no-path / expect-error): reading-nextjs: 46 / 0 / 10 ...A fence deliberately showing broken code (like most of the “wrong code” examples in the AI Bug Catalog) starts with // @expect-error <reason> so the harness skips it — the lesson prose carries the real, separately-captured build/tsc output instead of asking the harness to reproduce a deliberate failure on every run.
Mental Model
Section titled “Mental Model”flowchart TD
subgraph Static["Static (source/config only)"]
Tsc["tsc --noEmit<br/>real type errors: revalidateTag, typed params"]
Lint["ESLint (eslint-config-next) / Biome<br/>no-html-link-for-pages and friends"]
Verify["tools/verify-snippets.mjs<br/>compiles every tagged fence in the course"]
end
subgraph BuildTime["Build-time (needs a real next build)"]
Build["next build route table<br/>○ ● ◐ ƒ, plus warnings that don't fail CI"]
Debug["next build --debug-prerender<br/>real stack traces for prerender errors"]
Analyze["next experimental-analyze<br/>real bundle composition per route"]
Typegen["next typegen<br/>PageProps/LayoutProps without a full build"]
end
subgraph Runtime["Runtime (needs a real browser or server)"]
Rsc["RSC payload in the Network tab"]
Pw["Playwright smoke test<br/>post-hydration, post-navigation state"]
Instr["instrumentation.ts onRequestError<br/>in next dev, not just production"]
end