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

Debugging Next.js

Read the dev error overlay, find server and browser logs, pause server code with --inspect, fix hydration mismatches, and trace a production digest.

By the end, you can
  • 01
    Find where an error isRead the dev overlay's message, file and line, the dev indicator's issues, and what the terminal and Chrome's console printed.
  • 02
    Fix hydration mismatchesRead React's server and client diff, name the usual causes, and fix them with an Effect, next/dynamic or suppressHydrationWarning.
  • 03
    Debug the server and productionPause server code with next dev --inspect, and follow a production digest from the page to the server's log.

One bug, read in development

The tea shop's orders page has a bug: the function that reads the orders throws an error. When you work on the shop with next dev, the development server, Next.js shows you three things about that error, in three places. Learning where each one is, and what it tells you, is most of debugging a Next.js app.

This is a recording of a real Next.js 16.4.3 project. The recorder ran next dev, opened /orders in real Chrome, and kept what the server printed, what Chrome's console printed, and what the page showed. Open the Steps tab first.

The orders page with a bug, in next devNext.js
app/_data/orders/
app/_data/orders.ts
// A stand-in for the shop's orders database. Today it's down.export async function getOrders(): Promise<string[]> {  console.log("Loading the orders");  throw new Error("The orders database did not answer.");}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next dev. Pick one to see the page after it.

localhost:3000/orders
This page couldn’t load

A server error occurred. Reload to try again.

Reload

ERROR 279879128

Focus
the <nextjs-portal>
Scrolled
0 px down

Errors in Chrome's console during this step (1)

  1. Error: The orders database did not answer.

Everything Chrome's console printed during this step (3)

  1. [info] Download the React DevTools for a better development experience: https://react.dev/link/react-devtools
  2. [log] [HMR] connected
  3. [log] Server Loading the orders

What Next.js's dev overlay showed after this step (the indicator's badge: 1 issue)

Runtime Error
Server
The orders database did not answer.

app/_data/orders.ts (4:9) @ getOrders

  2 | export async function getOrders(): Promise<string[]> {
  3 |   console.log("Loading the orders");
> 4 |   throw new Error("The orders database did not answer.");
    |         ^
  5 | }
  6 |

Call Stack
7

Show 5 ignore-listed frame(s)
getOrders
app/_data/orders.ts (4:9)
OrderList
app/orders/page.tsx (7:33)

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

  1. GET/orders200a whole page
  2. POST/__nextjs_original-stack-frames200application/json

In app/_data/orders.ts, line 3 logs Loading the orders, and line 4 throws. The page that calls it, app/orders/page.tsx, waits for connection() on line 6, so its list is made for each request, inside a Suspense boundary. In the Steps tab:

  • The page shows Next.js's error screen, This page couldn’t load, with ERROR 279879128 at the bottom. That number is the error's digest, a short code for this error.
  • The dev overlay, a panel next dev draws on top of the page, opened by itself. It reads Runtime Error, the label Server, the message, and app/_data/orders.ts (4:9) @ getOrders: the file, line 4, character 9, in the function getOrders. Under it are the lines around line 4, with a ^ under the new Error.
  • Chrome's console printed Server Loading the orders: the server's console.log, shown again in the browser.

Now open the Terminal tab, which shows what next dev printed. The server's own line, Loading the orders, comes first. Then ⨯ Error: The orders database did not answer., with at getOrders (app/_data/orders.ts:4:9), the same lines of code, and digest: '279879128': the number on the page. Then GET /orders 200, the request, and last a [browser] line: Chrome sent its error back to the terminal.

Three windows for one error in next dev. The terminal shows Loading the orders, an error line, and digest 279879128. The browser's overlay shows Runtime Error, the label Server, and orders.ts (4:9). Chrome's console shows the label Server and Loading the orders.terminalLoading the orders⨯ Errordigest: '279879128'what the server printedoverlayRuntime ErrorServerorders.ts (4:9)the file and the lineconsoleServerLoading the ordersthe server's log, replayed
One error, three places. The terminal has what the server printed, with the digest. The overlay names the file and the line it was thrown on. Chrome's console shows the server's console.log again, with a Server label.
Real-life analogyA car at the garage

A car with a problem tells you about it in more than one way. A light on the dashboard names the part. The logbook says what happened and when. The driver remembers what it felt like from the seat. And when none of that is enough, the mechanic plugs in a computer and runs the engine one step at a time.

In real life: A warning light on the dashboard that names the part
In Next.js: The dev overlay: the message, the file and the line
In real life: The car's logbook, with every event written down
In Next.js: The terminal: what the server printed, with digests
In real life: What the driver noticed from the seat
In Next.js: Chrome's console: what happened in the browser
In real life: A mechanic's computer, plugged in, stopping the engine at one step
In Next.js: next dev --inspect: a debugger that pauses the server
The main idea

In development, Next.js shows an error in three places: the dev overlay in the browser, the terminal, and Chrome's console. The overlay names the file and line. The terminal has everything the server printed. The console has what ran in the browser. In production, the browser gets only a digest, and the message stays in the server's log.

The error overlay and the dev indicator

The overlay is part of next dev only: next start never shows it. Look at the one in the overview again. From the top:

  • What kind of error it is. Runtime Error is an error thrown while the app ran. A hydration error is a Recoverable Error, and an error React only logged is a Console Error.
  • Where the code ran. Server means the error came from the server's render. In development, Next.js 16.4 renders a page in internal stages and labels each line with the stage it came from. In these recordings, Prerender marked a page that could be made ahead of time, and Server the part made after connection(). Next.js has other internal stages too (for the shell, prefetches and navigations), some labeled Prefetch; they're details of this version, not an API to rely on.
  • The message, the file and the line, with a few lines of the code around it. The file names are your own files, not the built ones, because next dev maps the running code back to your source with source maps: files that say which line of your code each line of built code came from.
  • The call stack: the functions that were running, innermost first. Here it's getOrders, called from OrderList on line 7 of app/orders/page.tsx. The overlay hides the frames inside React and Next.js, and says how many: Show 5 ignore-listed frame(s).

When an error is shown, the small round button in the corner of the page, the dev indicator, gets a badge with the number of issues: the errors Next.js found on this page. The overview's step recorded 1. Some errors open the overlay by themselves, like this runtime error. Others, like a hydration error, only add to the badge, and you click the badge to open them.

The indicator is useful with no error too. Here is the healthy teas page. The recorder opened it, clicked the indicator (its name for screen readers is Open Next.js Dev Tools), then clicked Route Info. Open the Steps tab and pick each step:

The dev indicator's menu on the teas pageNext.js
app/teas/
app/teas/page.tsx
import AddButton from "./add-button"; const teas = ["Green tea", "Mint tea"]; export default function TeasPage() {  console.log(`TeasPage rendered with ${teas.length} teas`);  return (    <main>      <h1>Teas</h1>      <ul>        {teas.map((tea) => (          <li key={tea}>            {tea} <AddButton tea={tea} />          </li>        ))}      </ul>    </main>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next dev. Pick one to see the page after it.

localhost:3000/teasTea shop
Teas
  • Green tea Add Green tea
  • Mint tea Add Mint tea
Focus
the <nextjs-portal>
Scrolled
0 px down

What Next.js's dev overlay showed after this step

Bundler
Turbopack
Cache Components
Enabled
Navigation Inspector
Route Info
Preferences

No requests during this step: the page already had what it needed

Step 2 is the menu: Bundler Turbopack, Cache Components Enabled, then Navigation Inspector, Route Info and Preferences. Step 3 is Route Info: the route /teas and the files that make it, app/layout.tsx and app/teas/page.tsx. When a page shows something you didn't expect, this is a quick way to check which layout and page Next.js used.

You can move the indicator, or hide it, in next.config.ts. This is from the devIndicators reference on nextjs.org, not recorded:

next.config.ts: where the dev indicator sitsTypeScript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
  // Where the indicator sits: "bottom-left" (the default), "bottom-right", "top-left" or "top-right".
  devIndicators: { position: "bottom-right" },
  // Or hide it. Errors still open the overlay.
  // devIndicators: false,
};

export default nextConfig;

Line 6 moves it to the bottom right. With devIndicators: false on line 8, the indicator is gone, but the docs say Next.js still shows compile and runtime errors.

Server logs and browser logs

console.log is still the quickest way to see what your code does. In Next.js, the question is where the line appears, because components run in two places. Here is the teas page: TeasPage, a Server Component, logs when it renders, and AddButton, a Client Component, logs when it renders and when it's clicked.

The teas page's logs, in next devNext.js
app/teas/
app/teas/add-button.tsx
"use client"; import { useState } from "react"; export default function AddButton({ tea }: { tea: string }) {  const [count, setCount] = useState(0);  console.log(`AddButton for ${tea} rendered: ${count} in the cart`);  return (    <button      onClick={() => {        setCount(count + 1);        console.log(`Added ${tea}`);        console.warn(`Only 2 ${tea} left`);      }}    >      Add {tea}    </button>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next dev. Pick one to see the page after it.

localhost:3000/teasTea shop
Teas
  • Green tea Add Green tea
  • Mint tea Add Mint tea
Focus
the button “Add Mint tea”
Scrolled
0 px down

Everything Chrome's console printed during this step (4)

  1. [log] Added Mint tea
  2. [warning] Only 2 Mint tea left
  3. [log] AddButton for Mint tea rendered: 1 in the cart
  4. [log] AddButton for Mint tea rendered: 1 in the cart

Next.js's dev overlay was closed, with no issues on the indicator

No requests during this step: the page already had what it needed

In app/teas/add-button.tsx, line 7 logs every render, and in the click handler, line 12 logs Added Mint tea and line 13 warns Only 2 Mint tea left. The recorder opened /teas and clicked Add Mint tea. Pick the steps in the Steps tab, then open the Terminal tab:

  • The Server Component's line, TeasPage rendered with 2 teas, is in the terminal, and in Chrome's console as Prerender TeasPage rendered with 2 teas. In development, Next.js replays a Server Component's logs in the browser, with the stage label from the overlay: this page reads nothing from the request, so the label is Prerender.
  • The Client Component's render line is in both too. In the terminal, because Next.js renders Client Components on the server for the first HTML. In Chrome's console, twice per render, because in development React renders each component twice to help you find impure code (Keeping components pure). The terminal also has more render lines than one visit asks for: in development, Next.js may render a page more than once.
  • The click's lines run only in the browser, so step 2 of Chrome's console has both. The terminal has only one of them: [browser] Only 2 Mint tea left (app/teas/add-button.tsx:13:17), with the file and line.

That last line comes from logging.browserToTerminal, an option added in Next.js 16.2. Its default, 'warn', sends warnings and errors from the browser to the terminal; 'error' sends only errors; true sends every log; false sends none. It only works in next dev. Try the three setups below, and before you pick one, guess which lines reach the terminal in production.

Playground: where does each log line go?
The teas page, with this setupNext.js
app/teas/
app/teas/add-button.tsx
"use client"; import { useState } from "react"; export default function AddButton({ tea }: { tea: string }) {  const [count, setCount] = useState(0);  console.log(`AddButton for ${tea} rendered: ${count} in the cart`);  return (    <button      onClick={() => {        setCount(count + 1);        console.log(`Added ${tea}`);        console.warn(`Only 2 ${tea} left`);      }}    >      Add {tea}    </button>  );}
Recorded from Next.js 16.4.3
$ next dev▲ Next.js 16.4.3 (Turbopack)- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ Ready- Cache Components enabled- Partial Prefetching enabled TeasPage rendered with 2 teasAddButton for Green tea rendered: 0 in the cartAddButton for Mint tea rendered: 0 in the cart GET /teas 200AddButton for Green tea rendered: 0 in the cartAddButton for Mint tea rendered: 0 in the cartAddButton for Green tea rendered: 0 in the cartAddButton for Mint tea rendered: 0 in the cartAddButton for Green tea rendered: 0 in the cartAddButton for Mint tea rendered: 0 in the cartAddButton for Green tea rendered: 0 in the cartAddButton for Mint tea rendered: 0 in the cartAddButton for Green tea rendered: 0 in the cartAddButton for Mint tea rendered: 0 in the cart[browser] Only 2 Mint tea left (app/teas/add-button.tsx:13:17)
Try it yourself
next dev

The terminal has the server's renders and [browser] Only 2 Mint tea left: by default, warnings and errors are forwarded. Added Mint tea stays in Chrome's console.

Each setup was opened in real Chrome, which clicked Add Mint tea. Terminal shows what the server printed; Steps shows everything Chrome's console printed after each step. Reset goes back to next dev.

With browserToTerminal: true, the terminal also gets [browser] Added Mint tea and every render line. In production, the terminal of next start printed nothing for the visit: the teas page is prerendered, so TeasPage rendered with 2 teas is in the output of next build. A Server Component's log appears when it renders, and for a prerendered page that's the build. Chrome's console shows each line once: no replay, and no double render.

Each row is one line of code. Read across to see where it showed up in each recording:

Where each console line showed up
The lineIn next devIn next build and next start
TeasPage, a Server Component, while it rendersIn the terminal, and in Chrome's console with a Prerender label.Printed by next build, because the page was prerendered. Nothing while the page was used.
AddButton, a Client Component, while it rendersIn the terminal (its server render), and in Chrome's console twice per render.Printed by next build for the prerendered HTML, and once per render in Chrome's console.
console.log in the click handlerOnly in Chrome's console. With browserToTerminal: true, in the terminal too.Only in Chrome's console.
console.warn in the click handlerIn Chrome's console, and in the terminal with a [browser] label.Only in Chrome's console.

Pausing server code with --inspect

Logs show you what happened. A debugger lets you stop the code on a line and look around: the values of every variable at that moment, and the functions that called it. In the browser, Chrome's DevTools already does this for Client Components (Debugging in the browser). Server Components, Route Handlers and Server Actions run in Node.js, so you need to open Node's inspector: the part of Node.js a debugger connects to. Start the development server with --inspect:

next dev --inspect, on the orders pageNext.js
app/_data/orders/
app/_data/orders.ts
// A stand-in for the shop's orders database. Today it's down.export async function getOrders(): Promise<string[]> {  console.log("Loading the orders");  throw new Error("The orders database did not answer.");}
Recorded from Next.js 16.4.3
$ next dev --inspectDebugger listening on ws://127.0.0.1:9229/<id>For help, see: https://nodejs.org/learn/getting-started/debugging▲ Next.js 16.4.3 (Turbopack)- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000- Debugger port: 9229✓ Ready- Cache Components enabled- Partial Prefetching enabled Loading the orders⨯ Error: The orders database did not answer.    at getOrders (app/_data/orders.ts:4:9)    at OrderList (app/orders/page.tsx:7:33)  2 | export async function getOrders(): Promise<string[]> {  3 |   console.log("Loading the orders");> 4 |   throw new Error("The orders database did not answer.");    |         ^  5 | }  6 | {  digest: '279879128'}

The Terminal tab starts with Debugger listening on ws://127.0.0.1:9229/<id>: the inspector's address. 9229 is Node's default port, and the id is new on every run (the recorder used a free port and shows it as 9229). Next.js repeats the port in its own list: - Debugger port: 9229. The page still works: GET /orders answered 200, and the error was printed as before.

To pause the server, put a debugger statement where you want it to stop. Node.js ignores it unless a debugger is attached:

app/_data/orders.ts, with a debugger statementTypeScript
// A stand-in for the shop's orders database. Today it's down.
export async function getOrders(): Promise<string[]> {
  debugger;
  console.log("Loading the orders");
  throw new Error("The orders database did not answer.");
}

Line 3 is the debugger statement. Then attach a debugger. These steps are from Next.js's debugging guide, not recorded, since a recording can't click inside DevTools:

  1. In Chrome, open chrome://inspect. Your Next.js server is listed under Remote Target. Click inspect, and a separate DevTools window opens for the server.
  2. Open /orders. The server stops on line 3, and DevTools shows the file in its Sources tab, with the variables and the call stack. Press Resume to go on.
  3. Instead of a debugger statement, you can click a line number in the Sources tab to set a breakpoint. Search for a file with Ctrl+P (⌘+P on macOS).
Left, a terminal running next dev --inspect shows the address ws://127.0.0.1:9229. An arrow labeled chrome://inspect or VS Code attaches goes to a DevTools window on the right, which shows Paused on debugger, in orders.ts at line 3.terminalnext dev --inspectws://127.0.0.1:9229attachesDevToolsPaused on debuggerorders.ts, line 3the inspector's addresschrome://inspect or VS Code
next dev --inspect opens Node's inspector on port 9229. Chrome's chrome://inspect page or VS Code attaches to that address, and the server stops at the debugger statement on line 3 of app/_data/orders.ts until you press Resume.

VS Code can start the server and attach in one go. The guide gives a .vscode/launch.json with several setups; these are two of them:

.vscode/launch.json: debug the server, or the browserJSON
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Next.js: debug server-side",
      "type": "node-terminal",
      "request": "launch",
      "command": "npm run dev -- --inspect"
    },
    {
      "name": "Next.js: debug client-side",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:3000"
    }
  ]
}

Lines 4 to 9 are the server setup: VS Code runs npm run dev -- --inspect in its own terminal and attaches to the inspector. Lines 10 to 15 open Chrome on http://localhost:3000, with VS Code as its debugger, for Client Components. Pick a setup in VS Code's Run and Debug panel and press F5.

Other ways to start the inspectorText
# Start the development server with Node's inspector on
npm run dev -- --inspect

# Stop before any code runs, until a debugger attaches
NODE_OPTIONS=--inspect-brk npm run dev

# Let a debugger outside this computer attach, as in a Docker container
npm run dev -- --inspect=0.0.0.0

--inspect passes its value to Node.js. The guide says to use NODE_OPTIONS=--inspect-brk instead of a flag when you want the server to wait for a debugger before running any code, and --inspect=0.0.0.0 when the debugger is on another computer, like your laptop when the app runs in a Docker container. Only do that on a network you trust: whoever reaches the inspector can run code on the server.

Hydration mismatches

A Client Component runs twice for a page's first load: on the server, to make the HTML, and in the browser, where React hydrates it. Hydration is React rendering the Client Components again in the browser and matching the result to the HTML already on the screen, reusing that HTML and attaching the event handlers (Hydration, in the React course, goes deeper). Server Components never hydrate: their code doesn't reach the browser. When the browser's first render doesn't match the HTML, React can't reuse that part. It logs a hydration error and renders that part again in the browser.

Here is the most common way to cause one, from the shop's home page:

A menu size picked from the window, in next devNext.js
app/
app/menu.tsx
"use client"; export default function Menu() {  const size = typeof window === "undefined" ? "full" : window.innerWidth < 600 ? "small" : "large";  return <p>Menu size: {size}</p>;}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next dev. Pick one to see the page after it.

localhost:3000/Tea shop
Tea shop

Menu size: large

Focus
the <nextjs-portal>
Scrolled
0 px down

Chrome's console printed nothing during this step

What Next.js's dev overlay showed after this step (the indicator's badge: 1 issue)

Recoverable Error
Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.

It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.

See more info here: https://nextjs.org/docs/messages/react-hydration-error

client/server diff
+
Client
-
Server
<Menu>
  <p>
+   large
-   full

app/menu.tsx (5:10) @ Menu

  3 | export default function Menu() {
  4 |   const size = typeof window === "undefined" ? "full" : window.innerWidth < 600 ? "small" : "large";
> 5 |   return <p>Menu size: {size}</p>;
    |          ^
  6 | }
  7 |

Call Stack
14

Show 11 ignore-listed frame(s)
p
<anonymous>
Menu
app/menu.tsx (5:10)
Home
app/page.tsx (7:7)

No requests during this step: the page already had what it needed

Line 4 of app/menu.tsx checks typeof window. On the server there's no window, so it renders Menu size: full. In Chrome's 1024 pixel window it renders Menu size: large. The recorder opened the page until the dev indicator's badge said 1 Issue, then clicked the badge. Step 2 shows the overlay: Recoverable Error, React's message, which lists the usual causes, and then the client/server diff: + large for what the browser rendered, and - full for the server's HTML, inside <Menu> and <p>. Under it, the file and line, app/menu.tsx (5:10). The page ends up saying large: React rendered it again in the browser.

Three windows. The server's HTML says Menu size: full. The browser's first render says Menu size: large, and a label between them says they don't match. An arrow leads to the result, Menu size: large, which React rendered again in the browser.serverMenu size: fullthe HTML that arrived≠browserMenu size: largethe first renderpagelargerendered againRecoverable Error: Hydration failed
The server has no window, so it rendered full. The browser's first render gave large. They don't match, so React throws that part of the HTML away and renders it again in the browser, and logs the error.

React's message lists the usual causes, and Next.js's page about the error adds more. Four of them are easy to record. Before you pick each version, guess how many issues the badge shows.

Playground: what breaks hydration?
The shop's home page, with this versionNext.js
app/
app/menu.tsx
"use client"; export default function Menu() {  const size = typeof window === "undefined" ? "full" : window.innerWidth < 600 ? "small" : "large";  return <p>Menu size: {size}</p>;}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next dev. Pick one to see the page after it.

localhost:3000/Tea shop
Tea shop

Menu size: large

Focus
the <nextjs-portal>
Scrolled
0 px down

Chrome's console printed nothing during this step

What Next.js's dev overlay showed after this step (the indicator's badge: 1 issue)

Recoverable Error
Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.

It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.

See more info here: https://nextjs.org/docs/messages/react-hydration-error

client/server diff
+
Client
-
Server
<Menu>
  <p>
+   large
-   full

app/menu.tsx (5:10) @ Menu

  3 | export default function Menu() {
  4 |   const size = typeof window === "undefined" ? "full" : window.innerWidth < 600 ? "small" : "large";
> 5 |   return <p>Menu size: {size}</p>;
    |          ^
  6 | }
  7 |

Call Stack
14

Show 11 ignore-listed frame(s)
p
<anonymous>
Menu
app/menu.tsx (5:10)
Home
app/page.tsx (7:7)

No requests during this step: the page already had what it needed

Try it yourself
typeof window

Line 4 picks full on the server, where there's no window, and large in the 1024 pixel browser window. The overlay's diff shows + large and - full.

Each version ran on next dev from Next.js 16.4.3, in real Chrome: open the page until the badge counts its issues, then click it. Step 2 shows the overlay; step 1 shows Chrome's console. Reset goes back to the first version.

Each row is one version, in the order of the buttons:

Four causes, and what the dev overlay showed
CauseWhat differedWhat the badge counted
typeof window in renderThe server rendered full, the browser large.1 issue: Recoverable Error, with a diff.
Math.random() in renderA new code on each render.2 issues: Next.js's own error about Math.random(), and the hydration error.
Math.random() inside SuspenseA new code on each render.1 issue: the hydration error. Next.js's own error is gone, the mismatch isn't.
A <div> inside a <p>The browser moved the <div> out of the <p> before React ran.2 issues: React's warning about the nesting, and the hydration error.

The Math.random() versions show something new in Next.js 16 with Cache Components: Next.js catches a random value or the current time in a Client Component before it becomes a mismatch, because the value would be fixed in the prerendered HTML. The same error also stops next build. The error suggests Suspense, and the third version shows that Suspense stops Next.js's error but not the mismatch: the part is made for each request, but the server and the browser still make two different codes.

Two more causes the docs name can't be recorded here:

  • Browser extensions. Some extensions change the page's HTML before React runs, for example by adding an attribute to <body>. Try the page in a private window without extensions: if the error is gone, an extension caused it.
  • Dates and numbers formatted for a place. toLocaleDateString() uses the server's language and time zone on the server and the visitor's in the browser, so the text can differ.

The fix is to make the first render in the browser match the server's HTML. Next.js's page about the error gives three ways, and each one below was recorded with no issues:

Playground: three ways to fix a mismatch
The shop's home page, fixed this wayNext.js
app/
app/menu.tsx
"use client"; import { useEffect, useState } from "react"; export default function Menu() {  const [size, setSize] = useState("full");  useEffect(() => {    setSize(window.innerWidth < 600 ? "small" : "large");  }, []);  return <p>Menu size: {size}</p>;}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next dev. Pick one to see the page after it.

localhost:3000/Tea shop
Tea shop

Menu size: large

Focus
nothing: the page itself
Scrolled
0 px down

Everything Chrome's console printed during this step (2)

  1. [info] Download the React DevTools for a better development experience: https://react.dev/link/react-devtools
  2. [log] [HMR] connected

Next.js's dev overlay was closed, with no issues on the indicator

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

  1. GET/200a whole page
Try it yourself
useEffect

The first render says full, like the server's HTML. The Effect on lines 7 to 9 runs after hydration and changes it to large. No issues, and the page shows Menu size: large.

Each fix ran on next dev from Next.js 16.4.3, in real Chrome. Steps shows the page, Chrome's console and the dev indicator: no issues in any of them. Reset goes back to the first fix.
  • An Effect (Effects). Render what the server can render first, then change it in an Effect, which runs only in the browser, after hydration. In the first fix, line 6 starts with full, and lines 7 to 9 change it to the window's size. Use this for values only the browser has.
  • suppressHydrationWarning on one element whose own text may differ, like a time. The docs say it only works one level deep, and that React then doesn't fix that text: the page keeps the server's version. Use it rarely.
  • next/dynamic with ssr: false, for a component that can only render in the browser, like one that reads localStorage while it renders. The server renders the loading text instead. The docs say ssr: false only works in a Client Component, so app/menu.tsx starts with "use client".
app/menu.tsx: the fix with an EffectTSX
"use client";

import { useEffect, useState } from "react";

export default function Menu() {
  const [size, setSize] = useState("full");
  useEffect(() => {
    setSize(window.innerWidth < 600 ? "small" : "large");
  }, []);
  return <p>Menu size: {size}</p>;
}
app/menu.tsx: a time that may differTSX
"use client";

export default function Menu() {
  return (
    <p>
      Prices checked at <time suppressHydrationWarning>{new Date().toISOString()}</time>
    </p>
  );
}
app/menu.tsx: a browser-only componentTSX
"use client";

import dynamic from "next/dynamic";

const SavedTeas = dynamic(() => import("./saved-teas"), {
  ssr: false,
  loading: () => <p>Loading your saved teas…</p>,
});

export default function Menu() {
  return <SavedTeas />;
}

Debugging a production error

next build and next start run the app the way visitors get it, and it behaves differently when something fails. Try the orders page on both servers:

Playground: the same error in development and in production
The orders page, run with this commandNext.js
app/_data/orders/
app/_data/orders.ts
// A stand-in for the shop's orders database. Today it's down.export async function getOrders(): Promise<string[]> {  console.log("Loading the orders");  throw new Error("The orders database did not answer.");}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next dev. Pick one to see the page after it.

localhost:3000/orders
This page couldn’t load

A server error occurred. Reload to try again.

Reload

ERROR 279879128

Focus
the <nextjs-portal>
Scrolled
0 px down

Errors in Chrome's console during this step (1)

  1. Error: The orders database did not answer.

Everything Chrome's console printed during this step (3)

  1. [info] Download the React DevTools for a better development experience: https://react.dev/link/react-devtools
  2. [log] [HMR] connected
  3. [log] Server Loading the orders

What Next.js's dev overlay showed after this step (the indicator's badge: 1 issue)

Runtime Error
Server
The orders database did not answer.

app/_data/orders.ts (4:9) @ getOrders

  2 | export async function getOrders(): Promise<string[]> {
  3 |   console.log("Loading the orders");
> 4 |   throw new Error("The orders database did not answer.");
    |         ^
  5 | }
  6 |

Call Stack
7

Show 5 ignore-listed frame(s)
getOrders
app/_data/orders.ts (4:9)
OrderList
app/orders/page.tsx (7:33)

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

  1. GET/orders200a whole page
  2. POST/__nextjs_original-stack-frames200application/json
Try it yourself
next dev

The overlay names the message and app/_data/orders.ts (4:9), and the terminal prints the error with its digest. Chrome's console shows the server's Loading the orders line, labeled Server.

Both were opened in real Chrome. The first ran on next dev; the second was built with next build and run with next start, from Next.js 16.4.3. Steps shows the page, the console and the overlay; Terminal shows what the server printed. Reset goes back to next dev.

Each row asks the same question of both recordings:

The same error on next dev and on next start
Questionnext devnext start
What does the page show?Next.js's error screen, with the digest, and the overlay on top.The same error screen, with a digest. No overlay.
Does the browser get the message?Yes: the overlay shows it, with the file and line.No: Chrome's console shows React's error #441, without the message.
Where is the real message?In the overlay, and in the terminal.Only in the server's log, next to the digest.
Which file and line?app/_data/orders.ts (4:9), from source maps.Built chunk names like .next/server/chunks/ssr/…js:1:127.

In production, a Server Component's message never reaches the browser, because a message can hold things a visitor must not see. Chrome's console only has Minified React error #441: React's own messages are shortened to a number in production. The error screen shows the digest, ERROR 2415591236, and next start printed digest: '2415591236' under ⨯ Error: The orders database did not answer. That's how you debug it: the visitor reports the code on the screen, and you search the server's log for it. (Error handling shows an error.tsx that shows the digest in your own error screen.) The stack trace names built files, like .next/server/chunks/ssr/…js:1:127, so the message and the digest matter more than the line.

A hydration error is shortened in production too. The menu with typeof window, built and run with next start, logged only this in Chrome:

The window menu in productionNext.js
app/
app/menu.tsx
"use client"; export default function Menu() {  const size = typeof window === "undefined" ? "full" : window.innerWidth < 600 ? "small" : "large";  return <p>Menu size: {size}</p>;}
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/Tea shop
Tea shop

Menu size: large

Focus
nothing: the page itself
Scrolled
0 px down

Errors in Chrome's console during this step (1)

  1. Error: Minified React error #418; visit https://react.dev/errors/418?args[]=text&args[]= for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

Chrome's console printed nothing during this step

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

  1. GET/200a whole page

Minified React error #418 is the short form of "Hydration failed because the server rendered text didn't match the client". There's no diff and no file. To find the cause, run the same page in next dev, where the overlay shows the diff and the file.

Some errors happen in the build itself. With Cache Components on, the menu with Math.random() and no Suspense stops next build:

The random menu's buildNext.js
app/
app/menu.tsx
"use client"; export default function Menu() {  const code = Math.random().toString(36).slice(2, 8);  return <p>Discount code of the day: {code}</p>;}
next build failed · 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 ...Error: Route "/": Next.js encountered the unstable value `Math.random()` in a Client Component. This value would be evaluated during the prerender, instead of recomputed on each visit. Ways to fix this:  - [stream] Wrap the Client Component in `<Suspense fallback={...}>`  - [defer] Move the read into a `useEffect` or event handler Learn more: https://nextjs.org/docs/messages/blocking-prerender-random-client    at <unknown> (app/menu.tsx:4:21)  2 |  3 | export default function Menu() {> 4 |   const code = Math.random().toString(36).slice(2, 8);    |                     ^  5 |   return <p>Discount code of the day: {code}</p>;  6 | }  7 |To get a more detailed stack trace and pinpoint the issue, try one of the following:  - Start the app in development mode by running `next dev`, then open "/" in your browser to investigate the error.  - Rerun the production build with `next build --debug-prerender` to generate better stack traces.Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-errorExport encountered an error on /page: /, exiting the build.⨯ Next.js build worker exited with code: 1 and signal: null

The build names the route, the value, the file and the line, and two ways to fix it. Under them it suggests two ways to see more: open the page in next dev, or build again with --debug-prerender. The CLI reference on nextjs.org says that flag turns off the minifying of server code, makes source maps for it, and keeps building after the first prerender error, so you see all of them. It's for debugging only: never deploy a build made with it.

Build commands for a build that failsText
# Readable stack traces and code frames for prerender errors, and every error at once
next build --debug-prerender

# Build only some routes while you look for the problem
next build --debug-build-paths="app/page.tsx"

The second command builds only the routes you name, which is quicker while you look for a problem in a big app. Once the app runs in production, a log you search by hand isn't enough: instrumentation.ts and onRequestError send every server error, with its digest, to a service that collects them. That's Instrumentation and observability.

Coding agents and the MCP server

A coding agent is an AI assistant that edits your project and runs commands, like the ones in code editors. To fix a bug, it needs what you read on this page: the errors, the logs, the routes. The Model Context Protocol (MCP) is an open standard for giving an agent tools it can call. In Next.js 16, next dev answers MCP at /_next/mcp. The recorder asked it for its tools, the way an agent's client does: a POST with the JSON-RPC method tools/list. Open the Network tab:

The development server's MCP endpointNext.js
app/teas/
app/teas/page.tsx
import AddButton from "./add-button"; const teas = ["Green tea", "Mint tea"]; export default function TeasPage() {  console.log(`TeasPage rendered with ${teas.length} teas`);  return (    <main>      <h1>Teas</h1>      <ul>        {teas.map((tea) => (          <li key={tea}>            {tea} <AddButton tea={tea} />          </li>        ))}      </ul>    </main>  );}
Recorded from Next.js 16.4.3

POST /_next/mcp200

Sent with the request
content-type application/json
accept application/json, text/event-stream

The body sent with the request

{"jsonrpc":"2.0","id":1,"method":"tools/list"}
Response headers
cache-control no-cache
content-type text/event-stream
vary Accept-Encoding

The body that arrived, as it arrived

event: message
data: {"result":{"tools":[{"name":"get_project_metadata","description":"Returns the the metadata of this Next.js project, including project path, dev server URL, etc.","inputSchema":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"get_errors","description":"Get the current error state from the Next.js dev server, including Next.js global errors (e.g., next.config validation), browser runtime errors, and build errors with source-mapped stack traces","inputSchema":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"get_page_metadata","description":"Get runtime metadata about what contributes to the current page render from active browser sessions.","inputSchema":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"get_logs","description":"Get the path to the Next.js development log file. Returns the file path so the agent can read the logs directly.","inputSchema":{"type":"object","properties":{}}},{"name":"get_server_action_by_id","description":"Locates a Server Action by its ID in the server-reference-manifest.json. Returns the filename and export name for the action.","inputSchema":{"type":"object","properties":{"actionId":{"type":"string"}},"required":["actionId"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"get_routes","description":"Get all routes that will become entry points in the Next.js application by scanning the filesystem. Returns routes grouped by router type (appRouter, pagesRouter). Dynamic segments appear as [param] or [...slug] patterns. API routes are included in their respective routers (e.g., /api/* routes from pages/ are in pagesRouter). Optional parameter: routerType (\"app\" | \"pages\") - filter by specific router type, omit to get all routes.","inputSchema":{"type":"object","properties":{"routerType":{"type":"string","enum":["app","pages"]}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"get_request_insights","description":"Get recent App Router request insights captured by the local Next.js span recorder. Useful for debugging slow renders, server fetches, cache behavior, and request timelines without an external OTEL collector. Requires experimental.requestInsights.","inputSchema":{"type":"object","properties":{"requestId":{"type":"string"},"htmlRequestId":{"type":"string"}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"get_compilation_issues","description":"Build the module graph for all routes and return all compilation issues (resolve errors, missing modules, transform errors, etc.). Does not require a browser session. Covers all routes proactively.","inputSchema":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"compile_route","description":"Compile a specific route (page or API route) without making an HTTP request. Triggers the same on-demand compilation the dev server uses when a route is first visited. Useful for warming up the module graph, measuring compile time, or pre-compiling routes for memory benchmarking. Returns { routeSpecifier, issues } on success where routeSpecifier is the resolved route and issues contains any compilation warnings or errors. Returns an error if no matching route exists.","inputSchema":{"type":"object","properties":{"routeSpecifier":{"type":"string","description":"A route specifier as returned by the get_routes tool (e.g. \"/\", \"/blog/[slug]\", \"/api/users/[id]\"). Mutually exclusive with `path`; provide exactly one."},"path":{"type":"string","description":"A URL path on this site (e.g. \"/blog/hello-world\", \"/docs/a/b/c\"). Query strings are allowed and ignored. Do not include scheme/host/port. The path is resolved to its matching route specifier using the dev router's live route table. Mutually exclusive with `routeSpecifier`; provide exactly one."}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}]},"jsonrpc":"2.0","id":1}

The answer came back as text/event-stream, with nine tools: get_project_metadata, get_errors, get_page_metadata, get_logs, get_server_action_by_id, get_routes, get_request_insights, get_compilation_issues and compile_route. That's the list for this setup: the MCP guide says get_compilation_issues and compile_route work only with Turbopack (the default bundler here), and get_request_insights needs experimental.requestInsights. So an agent can ask the running server for the errors the overlay shows (with the browser's errors, as long as the page is open), the path of the log file, every route, or whether a route compiles, without running next build.

Agents connect through a small program, next-devtools-mcp (0.4.0 on npm on October 30, 2026). It finds the Next.js development servers running on your computer and passes the agent's calls to them. Next.js's MCP guide adds it to a project in an .mcp.json file at the project's root, like this one. This part is from the guide and wasn't recorded:

.mcp.json: connect a coding agent to next-devtools-mcpJSON
{
  "mcpServers": {
    "next-devtools": {
      "command": "npx",
      "args": ["-y", "next-devtools-mcp@latest"]
    }
  }
}

Line 4 runs the package with npx, and line 5 asks for its latest version. The guide lists the tools it gives an agent: nextjs_index finds the running servers, nextjs_call calls one of the tools above, nextjs_docs points the agent at the docs that come with your Next.js in node_modules/next/dist/docs/, and browser_eval points it at a command-line browser for checking pages. The endpoint is on by default in next dev; experimental.mcpServer: false in next.config.ts turns it off. The same docs explain that when next dev detects that a coding agent started it, it also writes an AGENTS.md file that points the agent at those docs (the agentRules option turns it off).

Keep the development server private

/_next/mcp answers anyone who can reach the development server, with no sign-in: the errors, the logs' path, every route, the project's folder and the files behind Server Actions. "Only in next dev" isn't "only on your computer". Without -H, next dev listens on every network interface (the Network line in its output is the address other computers can use), and a container's forwarded port or a tunnel publishes it further. Never expose a development server to the internet. Start it with next dev -H 127.0.0.1, or behind a firewall, and when other computers can reach it, set experimental.mcpServer: false. The same goes for --inspect.

An agent reads what you read

The MCP tools don't add new information. They give an agent the same overlay errors, logs and routes you read in the browser and the terminal, as text it can read. What you know about each message still applies when an agent reads it for you, and you still check its fix in the browser.

Where to look first

A habit saves time: before you change any code, decide where the answer is. In development, the overlay for errors and the terminal for the server. In the browser, Chrome's console for client code. In production, the server's log, by digest. Sort these situations from the tea shop by where you look first.

Where do you look first?
  • A Server Component throws while you work on it in next dev.
  • The badge says 1 Issue after a page loads, and you want React's server and client diff.
  • A console.log in a static Server Component, in production.
  • A visitor of the live shop sends you ERROR 2415591236 from the error screen.
  • A console.log in a click handler, in production.
  • A console.log in a Client Component's Effect, in production.
Try it yourself
0 of 6 correct

Sort each situation by the first place that has the answer.

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

And when a page renders something odd but shows no error, open the dev indicator's Route Info to check which files made it, then log the values you're unsure of, or put a debugger statement there and attach with --inspect.

Common mix-ups

  • "A Server Component's console.log shows up in the browser." Only in development, as a replay with a label. It runs on the server, and in production its line is only in the server's output, or in the build's if the page was prerendered.
  • "Every line logged twice means a bug." No. In development React renders each Client Component twice to find impure code, so each render line shows twice in Chrome's console. In production it showed once.
  • "The overlay shows up in production too." No. It's part of next dev. Production shows an error screen with a digest, and the message stays in the server's log.
  • "A typeof window check makes browser-only code safe." No. It makes the server and the browser render different things, which is a hydration mismatch. Use an Effect, or next/dynamic with ssr: false.
  • "Wrapping it in Suspense fixes a Math.random() mismatch." No. It stops Next.js's prerender error, but the server and the browser still make two different values. Move the random value into an Effect or an event handler.
  • "suppressHydrationWarning fixes the text." No. It only stops the warning for one element, and the page keeps the server's text.
  • "--inspect is safe to open to the internet." No. Whoever reaches the inspector can run code on the server. Keep it on your own computer, or a network you trust.

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-upRead the overlay's line

The overlay named app/_data/orders.ts (4:9) @ getOrders. Which line of this file threw?

Starter codeTypeScript
// A stand-in for the shop's orders database. Today it's down.
export async function getOrders(): Promise<string[]> {
  console.log("Loading the orders");
  throw new Error("The orders database did not answer.");
}

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

    Exercise 2 · Warm-upWhich line reaches the terminal?

    In next dev, with no logging option, you click Add Mint tea. Which of the two lines in the click handler shows up in the terminal?

    Starter codeTSX
    "use client";
    
    import { useState } from "react";
    
    export default function AddButton({ tea }: { tea: string }) {
      const [count, setCount] = useState(0);
      console.log(`AddButton for ${tea} rendered: ${count} in the cart`);
      return (
        <button
          onClick={() => {
            setCount(count + 1);
            console.log(`Added ${tea}`);
            console.warn(`Only 2 ${tea} left`);
          }}
        >
          Add {tea}
        </button>
      );
    }
    

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

      Exercise 3 · PracticeFix the menu size

      This menu makes a hydration error. Fix it so the server and the browser agree on the first render. Which hook do you use?

      Starter codeTSX
      "use client";
      
      export default function Menu() {
        const size = typeof window === "undefined" ? "full" : window.innerWidth < 600 ? "small" : "large";
        return <p>Menu size: {size}</p>;
      }
      

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

        Exercise 4 · PracticeFind the HTML that breaks hydration

        This page makes two issues in the dev indicator, and no code in it checks the window or the time. Which line causes them?

        Starter codeTSX
        export default function Home() {
          return (
            <main>
              <h1>Tea shop</h1>
              <p>
                <div>Mint tea is back in stock.</div>
              </p>
            </main>
          );
        }
        

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

          Exercise 5 · ChallengeA digest from a visitor

          A visitor of the live shop sends a screenshot of the orders page: This page couldn’t load, and ERROR 2415591236. Where do you find what went wrong?

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

            Exercise 6 · ChallengeDoes Suspense fix it?

            Next.js's error about this menu says to wrap it in Suspense. A teammate does. Does the hydration error go away?

            Starter codeTSX
            "use client";
            
            export default function Menu() {
              const code = Math.random().toString(36).slice(2, 8);
              return <p>Discount code of the day: {code}</p>;
            }
            

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

              Check your understanding

              For each question, think about where the code ran, and which server was running it.

              The Debugging Next.js quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Which command lets a debugger, like Chrome DevTools or VS Code, pause your server code?

                Choose an answer to see the explanation.

              2. Question 2 of 7In next dev, a Server Component calls console.log. Where does the line show up?

                Choose an answer to see the explanation.

              3. Question 3 of 7In next dev, this component makes a hydration error. What does the overlay's diff show?

                Read the code, then predictTSX
                "use client";
                
                export default function Menu() {
                  const size = typeof window === "undefined" ? "full" : window.innerWidth < 600 ? "small" : "large";
                  return <p>Menu size: {size}</p>;
                }
                

                Choose an answer to see the explanation.

              4. Question 4 of 7With Cache Components on, a Client Component calls Math.random() while it renders, with no Suspense above it. What does next build do?

                Choose an answer to see the explanation.

              5. Question 5 of 7A production error screen shows ERROR 2415591236. What is that number?

                Choose an answer to see the explanation.

              6. Question 6 of 7By default, which browser logs does next dev 16.4 print in the terminal?

                Choose an answer to see the explanation.

              7. Question 7 of 7What does a coding agent get from the development server's /_next/mcp endpoint?

                Choose an answer to see the explanation.

              Key takeaways

              • In next dev, an error shows in three places: the overlay (message, file, line, call stack), the terminal (what the server printed, with a digest) and Chrome's console.
              • The dev indicator counts the page's issues; click it to open the ones that didn't open by themselves. Its menu has Route Info, the files that made the page.
              • A Server Component logs on the server; next dev replays it in the browser with a label. A Client Component logs in both places. browserToTerminal forwards the browser's warnings and errors to the terminal by default.
              • next dev --inspect opens Node's inspector on port 9229; attach with chrome://inspect or VS Code, and stop on a debugger statement.
              • A hydration mismatch is a browser's first render that differs from the server's HTML: typeof window, random values, the time, invalid HTML, extensions. Fix it with an Effect, next/dynamic with ssr: false, or, for one element, suppressHydrationWarning.
              • In production, the browser gets a digest and React's error number; the message is in the server's log next to the same digest. --debug-prerender makes build errors readable.

              Remember the one-liner.
              Read the overlay in development, the server's log in production, and make the browser's first render match the server's HTML.

              Read it in Next.js's own code

              The labels in the overlay and in Chrome's console come from getEnvironmentNameForStage, at lines 5494 to 5515 of app-render.tsx in release v16.4.3: each stage of a development render is named Prerender, Prefetch or Server.

              Coming next: Instrumentation and observability: sending a running app's errors, logs and traces to the services that collect them, with instrumentation.ts, onRequestError and OpenTelemetry.

              CompleteFrontend Clear concepts. Working examples.