Type something to search...
Next.js Debugging tools

Next.js Debugging tools

Most developers debug a Next.js app the same way they'd debug any React project: sprinkle in a few console.log calls, stare at the output, delete the logs, repeat. It works, right up until the bug lives in code that runs on the server, inside a Server Action, or somewhere in the gap between a Server Component and the client bundle it hands off to. At that point, console.log either prints to the wrong place (your terminal instead of your browser, or vice versa) or doesn't print at all, and you're left guessing.

Next.js runs on Node.js under the hood, which means every debugger that can attach to a Node process, VS Code, WebStorm, Chrome DevTools, Firefox DevTools, can attach to your Next.js app too. The tricky part isn't whether you can debug it, it's knowing which tool to point at which half of your code, because a Next.js app is really two runtimes wearing a trench coat: a server process and a browser bundle, and each one needs a different debugging setup.

This guide walks through both halves, plus a handful of things that trip people up the first time they try to attach a debugger to a real Next.js project rather than a toy example.

Why This Matters More in Next.js Than in Plain React

In a client-only React app, "debugging" almost always means opening Chrome DevTools and setting a breakpoint. There's one JavaScript runtime, one bundle, one place to look.

Next.js breaks that assumption in two ways. First, a meaningful chunk of your code, Server Components, Route Handlers, Server Actions, middleware, never reaches the browser at all. It executes in a Node.js process on your machine (or on a server, in production) and its console.log output shows up in your terminal, not your browser console. If you don't know that going in, you'll spend ten minutes wondering why your breakpoint in Chrome never triggers, when the answer is that the code you're looking at simply isn't client-side code.

Second, source maps have to bridge two different bundlers' output, one for the server bundle, one for the client bundle, back to your original TypeScript or JSX files. Get the debugger configuration wrong and you'll be stepping through minified webpack://_N_E/./... paths instead of your actual source, which is a miserable way to find a bug.

Once you internalize "server code runs in Node, client code runs in the browser, and each needs its own debugger attached to it," the rest of this is just configuration.

Debugging with VS Code

VS Code's built-in debugger is the most common setup because it can drive both halves, server and client, from one place, and even attach to both simultaneously with a single "compound" launch configuration.

Create .vscode/launch.json at the root of your project:

// .vscode/launch.json
{
  "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"
    },
    {
      "name": "Next.js: debug client-side (Firefox)",
      "type": "firefox",
      "request": "launch",
      "url": "http://localhost:3000",
      "reAttach": true,
      "pathMappings": [
        {
          "url": "webpack://_N_E",
          "path": "${workspaceFolder}"
        }
      ]
    },
    {
      "name": "Next.js: debug full stack",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/node_modules/next/dist/bin/next",
      "runtimeArgs": ["--inspect"],
      "skipFiles": ["<node_internals>/**"],
      "serverReadyAction": {
        "action": "debugWithEdge",
        "killOnServerStop": true,
        "pattern": "- Local:.+(https?://.+)",
        "uriFormat": "%s",
        "webRoot": "${workspaceFolder}"
      }
    }
  ]
}

Swap npm run dev for yarn dev or pnpm dev depending on your package manager. If you want Firefox debugging inside VS Code, install the Firefox Debugger extension first, the configuration above won't do anything without it.

The "full stack" configuration is the interesting one. It launches the Next.js binary directly with --inspect, waits for the dev server to print its "Local:" URL, then automatically opens a browser and attaches a debugger to both the Node process and the browser tab at once. serverReadyAction.action controls which browser opens, debugWithEdge opens Edge; change it to debugWithChrome if that's your daily driver.

A few adjustments you'll likely need in a real project:

Custom port: if your dev server runs on something other than 3000, replace every 3000 in the config with your actual port.

Monorepo / non-root directory: if you're running Next.js from a subdirectory, Turborepo-style, add a cwd key to the server-side and full-stack configurations, for example "cwd": "${workspaceFolder}/apps/web". Without it, VS Code runs the launch command from the repo root, npm run dev resolves to the wrong package.json, and you get a confusing "command not found" instead of a debugger session.

To start debugging, open the Debug panel (Ctrl+Shift+D on Windows/Linux, ⇧+⌘+D on macOS), pick a configuration from the dropdown, and press F5.

The gotcha this project would actually hit

Here's something the docs don't mention, and it's worth knowing if your dev script isn't a plain next dev call. This repo's own package.json, for example, defines:

"dev": "concurrently \"node scripts/themeGenerator.mjs --watch\" \"npm run generate-json && next dev\""

Run npm run dev -- --inspect against a script like that, and the --inspect flag gets forwarded to concurrently and its child processes in a way that doesn't cleanly attach to the actual next dev process, concurrently isn't Next.js, it doesn't know what to do with a Node inspector flag, and depending on your version it may silently swallow it or crash on an unrecognized argument.

If your dev script is wrapped in concurrently, npm-run-all, or anything similar, the reliable fix is to add a second, debug-only script that skips the wrapper:

"dev:debug": "npm run generate-json && next dev --inspect"

And point your VS Code launch config at that instead:

{
  "name": "Next.js: debug server-side",
  "type": "node-terminal",
  "request": "launch",
  "command": "npm run dev:debug"
}

This is a five-minute fix, but it's the kind of thing that costs people an hour if they don't know to look for it, because the failure mode isn't an error, it's just silence: no "Debugger listening" message ever appears in the terminal, and no amount of staring at your breakpoints in VS Code will fix that, because the debugger was never attached to anything in the first place.

Debugging in JetBrains WebStorm

WebStorm's flow is shorter but conceptually the same. Open the run configuration dropdown, choose Edit Configurations, and add a new JavaScript Debug configuration pointed at http://localhost:3000. Give it a name, decide which browser it should launch, and save it as a project file if you want teammates to share the same setup.

Run that configuration and WebStorm opens the chosen browser automatically. At this point you have two things running in debug mode simultaneously: the Next.js Node process and the browser tab. WebStorm doesn't distinguish "server breakpoint" from "client breakpoint" the way VS Code's separate configurations do, breakpoints you set in server files and client files both just work, because WebStorm's debugger is attached to both ends already.

Debugging with Browser DevTools Directly

You don't need an IDE at all for a lot of debugging. Both Chrome and Firefox can attach directly, and for quick client-side issues this is often faster than switching to VS Code.

Client-side code

Start your dev server as usual (next dev, npm run dev, whatever your script is called) and open http://localhost:3000 in your browser.

In Chrome, open DevTools (Ctrl+Shift+J / ⌥+⌘+I) and go to the Sources tab. In Firefox, open DevTools (Ctrl+Shift+I / ⌥+⌘+I) and go to the Debugger tab. Either way, any debugger statement in your client-side code will pause execution and jump you straight to that file:

"use client";

export function SearchBox() {
  function handleChange(value: string) {
    debugger; // execution pauses here when this fires
    // ...
  }

  return <input onChange={(e) => handleChange(e.target.value)} />;
}

You can also search for files manually and set breakpoints without touching your code: Ctrl+P in Chrome, Ctrl+P or the file tree in Firefox. One thing worth knowing ahead of time, your source files won't show up under their normal project paths. They'll be nested under something like webpack://_N_E/./app/components/SearchBox.tsx, that _N_E prefix is just Next.js's internal module namespace, and it's normal, not a sign that source maps are broken.

React Developer Tools

For anything React-specific, component tree issues, props not updating, unexpected re-renders, install the React Developer Tools browser extension. It gives you a component inspector, the ability to edit props and state live, and a profiler for spotting unnecessary re-renders. It's not Next.js-specific, but it's essential enough that skipping it makes client-side debugging noticeably harder.

One practical note: React DevTools shows Server Components differently from Client Components in its tree, Server Components appear but you can't inspect their props/state interactively the way you can with Client Components, since by the time the tree reaches the browser, Server Components have already been rendered to their output. If you need to inspect what a Server Component received as props, you're back to a server-side console.log or a server-side breakpoint, not the browser extension.

Server-side code

To reach server-side code with browser DevTools, start the dev server with the --inspect flag:

# npm
npm run dev -- --inspect

# yarn
yarn dev --inspect

# pnpm
pnpm dev --inspect

# bun
bun run dev --inspect

That flag is passed straight through to the underlying Node process, anything in Node's --inspect docs applies here too. One option worth calling out explicitly: --inspect=0.0.0.0 opens the inspector to remote connections instead of just localhost, which is what you need if you're running the dev server inside Docker and debugging from the host machine. Don't leave that flag on for anything reachable from outside a trusted network, --inspect=0.0.0.0 with no additional protection means anyone who can reach that port can attach a debugger to your running process and execute arbitrary code in it. Treat it the way you'd treat an open database port: fine on a locked-down local network, never on a public interface.

When it starts correctly, you'll see something like this in your terminal:

Debugger listening on ws://127.0.0.1:9229/0cf90313-350d-4466-a748-cd60f4e47c95
For help, see: https://nodejs.org/learn/getting-started/debugging
ready - started server on 0.0.0.0:3000, url: http://localhost:3000

That "Debugger listening" line is your confirmation the inspector actually attached. If you don't see it, --inspect didn't reach the right process, go back and check whether something in your dev script (like the concurrently example above) is intercepting the flag.

From there:

Chrome: open a new tab, go to chrome://inspect, find your app under Remote Target, and click inspect. That opens a dedicated DevTools window for the server process, go to Sources the same as you would for client code.

Firefox: open about:debugging, click This Firefox, find your app under Remote Targets, and click Inspect, then go to the Debugger tab.

File paths here follow a slightly different pattern than the client bundle: webpack://{application-name}/./, where {application-name} comes from the name field in your package.json. If you've never noticed that field mattering before, this is one of the few places it actually shows up in your workflow.

If you need execution to pause immediately when the process starts, rather than waiting for you to open the inspector window and set a breakpoint, use --inspect-brk instead of --inspect. Because of how the flag gets passed through the dev script, you may need to set it via NODE_OPTIONS rather than as a CLI argument:

NODE_OPTIONS=--inspect-brk next dev

This is genuinely useful for debugging something that goes wrong during the very first request or during server startup, boot-time config errors, a broken environment variable, anything that happens before you'd normally have time to open DevTools and attach.

Inspecting server errors from the error overlay

Here's a small but genuinely handy feature: when a server-side error surfaces in Next.js's dev error overlay, look for a Node.js icon underneath the Next.js version indicator. Clicking it copies a DevTools URL for that specific server process straight to your clipboard. Paste that into a new browser tab and you're immediately in the Sources tab for the process that threw the error, no need to manually go find it in chrome://inspect. It's a small shortcut, but it saves the "which of my seventeen open terminal tabs is actually running the dev server" hunt.

A note for Windows users

If you're developing on Windows and Fast Refresh feels sluggish, check whether Windows Defender is scanning your project directory in real time. Real-time protection inspects every file read, and Next.js's dev server reads a lot of files during Fast Refresh, this is a known source of slowdown that has nothing to do with Next.js itself, but shows up as "Next.js dev feels slow" in searches and bug reports. Excluding your project directory (or at least node_modules and .next) from real-time scanning is the usual fix, not disabling Defender project-wide.

Practical Notes the Docs Don't Cover

console.log placement is your first debugging signal, use it deliberately. Before reaching for a full debugger session, it's worth remembering that where a log statement appears tells you something. A console.log inside a Server Component, Server Action, or Route Handler prints to your terminal, the one running next dev. A console.log inside a Client Component, or inside a useEffect, prints to the browser console. If you're staring at an empty browser console wondering why your log never showed up, check your terminal first, there's a good chance the code you're debugging never left the server.

Breakpoints in async Server Components can behave unexpectedly under Turbopack's fast refresh. Because Server Components can await data directly in the component body, and because dev-mode fast refresh may re-invoke a component more than once as it settles, a breakpoint set inside a data-fetching Server Component can trigger multiple times for what feels like a single navigation. Don't assume your component ran twice in production just because your breakpoint fired twice in dev, check the behavior with --inspect disabled and a plain console.log with a timestamp before concluding anything about production behavior from a dev debugging session.

Server Actions need explicit attention if you're chasing a bug in a form submission. Because a Server Action executes on the server in response to a client-triggered event, the temptation is to set a breakpoint in the browser at the onSubmit handler and expect to "step into" the action call. You can't, the moment the action is invoked, execution jumps to the Node process, and your browser debugger has no visibility into it. Set your breakpoint (or your debugger statement, or your --inspect session) on the server side, in the action function itself, not in the client component that calls it.

Source maps for the server bundle and client bundle are genuinely separate, don't assume a fix on one side maps to the other. If you customize productionBrowserSourceMaps or adjust Turbopack/webpack configuration in next.config.js, remember that setting typically affects the client bundle. Server-side source maps are controlled separately and are generally on by default in development regardless. If stack traces in your terminal look unminified but stack traces in the browser look mangled (or vice versa), that's a sign you've only fixed source maps for one half of the app.

Middleware and Proxy execution isn't visible in the browser at all. Code in proxy.ts (the file that replaced middleware.ts in recent versions) runs in the Edge runtime by default, which has its own constraints and its own debugging story, console.log output from Proxy shows up in your terminal, and full Node inspector attachment doesn't apply the same way it does to the regular server runtime. If you need to step through Proxy logic with a real debugger rather than log statements, check whether your project has switched that route to the Node.js runtime; Edge-runtime debugging support is much more limited.

A compound launch configuration saves you from juggling two debugger windows. VS Code lets you define a compounds array in launch.json that starts multiple configurations together, so "server-side" and "client-side" launch as one action instead of two. If you're debugging an issue that spans both (a Server Action that behaves correctly on the server but the client never reflects the result, for instance), this is worth setting up once rather than manually starting two debug sessions every time.

Choosing the Right Tool for the Job

SymptomWhere to look
Client component renders wrong data or doesn't updateChrome/Firefox DevTools, Sources/Debugger tab, or React DevTools
console.log never appears in the browserCheck your terminal, it's probably server-side code
Bug only happens in a Server Action or Route Handler--inspect + chrome://inspect or about:debugging, or VS Code's server-side config
Need to pause before the first request completes--inspect-brk via NODE_OPTIONS
Bug spans both server and clientVS Code "full stack" config, or a compound launch config
Dev server feels slow (Windows only)Exclude the project from Windows Defender real-time scanning
Error overlay shows a server errorClick the Node.js icon under the version indicator for a direct DevTools link

Key Takeaways

Debugging a Next.js app comes down to one mental model: know which runtime your code actually executes in before you go looking for it. Client Components, event handlers, and anything wrapped in "use client" live in the browser, open DevTools or React DevTools. Server Components, Route Handlers, Server Actions, and Proxy live in Node (or the Edge runtime), you need --inspect, a Node-aware debugger, or your terminal.

VS Code's launch.json gives you the most control, especially the "full stack" configuration that attaches to both halves at once, but it requires you to point it at the actual next dev process, not a wrapper script that swallows the --inspect flag before it gets there. Browser DevTools work well for quick, isolated debugging on either side once you remember which port and which URL to attach to.

None of this is exotic once you've done it a couple of times, but the first time you try to debug a Server Action by setting a breakpoint in the browser and nothing happens, it's worth remembering: it's not broken, you're just looking in the wrong runtime.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper