Published Sep 16, 2025 ⦁ Updated Nov 10, 2026 ⦁ 6 min read

Node.js Debugger Guide: VS Code, Chrome and CLI (2026)

The best debugging tool for Node.js applications is the one Node already ships. Start your app with node --inspect app.js, then attach a Node debugger: Chrome DevTools, VS Code and WebStorm all connect to that same built-in inspector. For daily work, open a JavaScript Debug Terminal in VS Code and run your app as usual; breakpoints set in the editor will hit. On a server with no GUI, node inspect app.js gives you a command-line debugger with nothing to install.

This guide to debugging Node.js goes tool by tool, then problem by problem: crashes, hangs, memory leaks, slow endpoints, flaky tests, TypeScript, Docker and production. The output blocks come from real runs on Node v26.7.0 (macOS, arm64) on November 10, 2026, with long paths shortened. As of November 2026, Node 26 "Lithium" is the Active LTS line (since October 28, 2026), Node 24 and 22 are in Maintenance, and Node 27 is in Alpha, per the Node.js release schedule.

9229
default inspector port, bound to 127.0.0.1
v26
Active LTS line since October 28, 2026 (codename Lithium)
10
stack frames kept per error by default (--stack-trace-limit)
1,000 µs
default sampling interval of --cpu-prof
v24.12
Node 24 release where running .ts files became stable (also v25.2.0)
2019
year of ndb's last npm release (1.1.5); its repository is archived

Node.js debuggers compared

Every nodejs debugger worth using today is a client for the same built-in inspector, so you are choosing an interface, not an engine. The tools that predate the inspector have been abandoned.

Debugger What it is Price or license Best for Honest limitation Status (November 2026)
VS Code JavaScript debugger (js-debug) Built into VS Code Free; js-debug is MIT Daily breakpoints, logpoints, tests, TypeScript Cannot open .heapsnapshot files Active
Chrome DevTools via chrome://inspect DevTools attached to your Node process Free, in Chrome and Edge CPU profiles, heap snapshots Separate from your editor, no test runner integration Active
WebStorm and other JetBrains IDEs IDE with Node.js run and attach configurations Free for non-commercial use; commercial individual plan listed at $79 for the first year, or $7.90 billed monthly (pricing) Teams already on JetBrains Commercial work needs a subscription Active
node inspect CLI debugger built into Node Free SSH sessions, containers Simple stepping and inspection only, per the docs Stable
node inspect --probe Prints values at a line without pausing Free, Node 26.1.0 and later Printf-style debugging with no code edits Can only launch a new process Experimental
node-inspector Bridge from DevTools to the pre-inspector protocol BSD-2-Clause Nothing today Last npm release 1.1.2, January 2018 Unmaintained
ndb Google Chrome Labs debugger built on DevTools Apache-2.0 Nothing today Last npm release 1.1.5, September 2019 Archived

Pick a tool by symptom

Node debugging goes faster when you start from the symptom instead of from your favorite tool. Each row has its own section below.

Symptom Start with Command or action
Wrong value or wrong branch Breakpoint or logpoint JavaScript Debug Terminal, or node inspect --probe app.js:42 --expr "order" app.js
Crash with an exception Diagnostic report node --report-uncaught-exception app.js
Crash during start-up Pause before the first line node --inspect-brk app.js
Hang, or a process that will not exit Report on signal node --report-on-signal app.js, then kill -USR2 <pid>
Memory grows over time Two heap snapshots, compared node --heapsnapshot-signal=SIGUSR2 app.js
"JavaScript heap out of memory" Snapshots near the limit node --heapsnapshot-near-heap-limit=3 app.js
Slow endpoint or high CPU CPU profile node --cpu-prof app.js
Flaky test One file, one worker, under the debugger vitest --inspect-brk --no-file-parallelism
Stack trace points at compiled JavaScript Source maps node --enable-source-maps dist/index.js
Only happens in production Logs and reports first NODE_DEBUG, DEBUG, diagnostic reports

How the Node inspector works

Node embeds V8's inspector. When you pass --inspect, Node opens a WebSocket server and speaks the Chrome DevTools Protocol to whatever client connects (CLI docs). --inspect is the Node.js command-line flag that exposes the debugger interface. The other flags only change when your code starts running.

Flag or signal What it does Added in
--inspect[=[host:]port] Activates the inspector on 127.0.0.1:9229; code runs immediately. Port 0 picks a random free port v6.3.0
--inspect-brk[=[host:]port] Same, and breaks at the start of your script v7.6.0
--inspect-wait[=[host:]port] Same, and waits for a debugger to attach before running v22.2.0, v20.15.0
kill -USR1 <pid> Starts the inspector in a process that is already running (signal events)
--disable-sigusr1 Turns that signal off v23.7.0, v22.14.0

The Node.js inspector prints its address when it starts:

$ node --inspect server.js
Debugger listening on ws://127.0.0.1:9229/c779f25e-6926-447c-aa61-9b36fef0c1c0
For help, see: https://nodejs.org/learn/getting-started/debugging
listening on http://localhost:3000

The line always has this shape. Only the ID at the end changes from run to run:

node --inspect-brk server.js
# Debugger listening on ws://127.0.0.1:9229/0f2c936f-b1cd-4ac9-aab3-f63b0f33d55e

Use node --inspect-brk or --inspect-wait when the bug happens at start-up. With plain --inspect, the code runs past your breakpoints before you can attach.

Three details save time later:

  • The default Node.js debug port, 9229, listens on 127.0.0.1 only. Other machines cannot reach it.
  • Clients find the random ID over HTTP. That is how chrome://inspect and VS Code discover your process:
$ curl -s http://127.0.0.1:9229/json/list
[ {
  "description": "node.js instance",
  "id": "c779f25e-6926-447c-aa61-9b36fef0c1c0",
  "title": "server.js",
  "type": "node",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9229/c779f25e-6926-447c-aa61-9b36fef0c1c0"
} ]
  • Connect by IP address or localhost, never by another host name. To block DNS rebinding, Node requires the Host header to be an IP address or exactly localhost (debugging guide). In the test run, any other host name got HTTP 400 and WebSockets request was expected.

You can also switch the inspector on in a process started without any flag. Sending SIGUSR1 to a plain node server.js made it print the same Debugger listening line, with no restart.

Method 1: Debug Node.js in VS Code

VS Code ships its own JavaScript debugger, js-debug, so there is nothing to install. Its Node.js debugging docs describe three ways in.

Fastest: the JavaScript Debug Terminal

  1. Open the Command Palette (Ctrl+Shift+P, or Cmd+Shift+P on a Mac).
  2. Run Debug: JavaScript Debug Terminal (the docs list it as "Create JavaScript Debug Terminal").
  3. In that terminal, start your app the usual way: node app.js, npm run dev, npx tsx src/index.ts, npx vitest.
  4. Set breakpoints by clicking in the editor gutter. They hit.

Per the docs, VS Code debugs any Node.js process you start in that terminal. That is also how you debug npm run dev: no launch config at all.

Repeatable: a launch.json

For a setup the whole team shares, create .vscode/launch.json. This file combines four configurations from the VS Code docs: launch a program, launch through an npm script, attach to a port, and attach to a process you pick from a list.

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Program",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/src/server.js",
      "env": { "NODE_ENV": "development" },
      "skipFiles": ["<node_internals>/**"],
      "console": "integratedTerminal"
    },
    {
      "name": "Launch via npm",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run-script", "debug"]
    },
    {
      "name": "Attach to Process",
      "type": "node",
      "request": "attach",
      "port": 9229,
      "restart": true
    },
    {
      "name": "Attach by Process ID",
      "type": "node",
      "request": "attach",
      "processId": "${command:PickProcess}"
    }
  ]
}

Press F5 to start the selected configuration. "Launch via npm" expects a script named debug in package.json, for example "debug": "node src/server.js". "Attach by Process ID" works even on a process that was not started in debug mode, because VS Code sends it the USR1 signal described above.

The attributes you will touch most: runtimeExecutable and runtimeArgs (run through npm, tsx or another tool), env and envFile, console (use integratedTerminal if your program reads stdin), skipFiles (step over <node_internals>/** and dependencies), outFiles (where compiled JavaScript and its source maps live) and, for attach, localRoot and remoteRoot.

Auto Attach

With the Toggle Auto Attach command, VS Code attaches to Node processes started in its normal integrated terminal. The setting debug.javascript.autoAttachFilter has three modes:

  • smart: debug scripts outside node_modules, plus known runners such as mocha and ts-node.
  • always: debug every Node process started in the terminal.
  • onlyWithFlag: debug only processes started with --inspect or --inspect-brk.

After you turn it on, open a new terminal.

Breakpoints that do more than pause

Right-click the gutter instead of clicking it and you get four more kinds:

  • Conditional breakpoint: pauses only when an expression is truthy, such as order.total < 0.
  • Logpoint: logs a message to the Debug Console without pausing. Curly braces interpolate expressions: total is {order.total}. It replaces most temporary console.log calls.
  • Hit count: pauses after a number of hits. %2 breaks on every other hit, >10 after ten.
  • Triggered breakpoint: stays off until another breakpoint is hit.

Restart on save with node --watch

node --watch server.js restarts the process when a file it imported changes. Watch mode has been stable since v22.0.0 and v20.13.0 (CLI docs). Combined with the inspector, every restart gets a new ID:

$ node --watch --inspect server.js
Debugger listening on ws://127.0.0.1:9229/bd75111b-5a79-43ca-97a8-b0c5e12effcc
For help, see: https://nodejs.org/learn/getting-started/debugging
listening on http://localhost:3000
Change detected in '/app/server.js'
Restarting 'server.js'
Debugger listening on ws://127.0.0.1:9229/528db96b-4fc6-4c30-bc02-fea061ec53bb
For help, see: https://nodejs.org/learn/getting-started/debugging
listening on http://localhost:3000

That is why the "Attach to Process" configuration above sets "restart": true. Or skip the configuration and run node --watch server.js inside a JavaScript Debug Terminal.

Method 2: Debug Node.js in Chrome DevTools

Chrome and Edge can debug Node directly. You do not need a Node.js Chrome extension for any of this.

Node inspector: Chrome DevTools steps

  1. Start your app with node --inspect app.js, or node --inspect-brk app.js to pause on the first line.
  2. Open chrome://inspect. Node processes show up under Remote Target. In Microsoft Edge the page is edge://inspect.
  3. Click inspect under your script. If it is missing, click Configure and make sure your host and port, such as localhost:9229, are listed.
  4. Or click Open dedicated DevTools for Node.

This window is the closest thing to Node dev tools in the browser sense, and the panels behave as they do in front-end work:

  • Sources: breakpoints, conditional breakpoints, logpoints and stepping.
  • Memory: heap snapshots and the comparison view used for memory leaks.
  • Performance: record a CPU profile while you hit a slow endpoint. The Chrome docs note that the older JS Profiler panel is deprecated in favor of this one.

The experimental --experimental-network-inspection flag (v22.6.0, v20.18.0) adds your process's network activity to the window. If DevTools itself is new to you, our Chrome DevTools guide covers the panels, and the Redux DevTools guide covers state debugging on the front-end side of the same app.

Method 3: The command-line debugger

On a server or inside a container, use the built-in CLI client:

node inspect app.js

Here is a full session on a script with a real bug. One cart item's shipping cost arrives as a string, so + concatenates and the script prints Total: 53.99194.00.

// cart.js
const cart = [
  { name: 'keyboard', price: 49.99, qty: 1, shipping: 4 },
  { name: 'cable', price: 9.5, qty: 2, shipping: '4.00' }, // came from a form field
];

function total(items) {
  let sum = 0;
  for (const item of items) {
    sum += item.price * item.qty + item.shipping;
  }
  return sum;
}

console.log('Total:', total(cart));

Set a breakpoint on line 10, watch two expressions and continue twice (the listing printed after sb(10) is trimmed):

$ node inspect cart.js
< Debugger listening on ws://127.0.0.1:9229/728f3f32-8f1f-47e7-8784-e0c790f39b9e
< For help, see: https://nodejs.org/learn/getting-started/debugging
<
connecting to 127.0.0.1:9229 ... ok
< Debugger attached.
<
Break on start in cart.js:2
  1 // cart.js
> 2 const cart = [
  3   { name: 'keyboard', price: 49.99, qty: 1, shipping: 4 },
  4   { name: 'cable', price: 9.5, qty: 2, shipping: '4.00' }, // came from a form field
debug> sb(10)
debug> watch('sum')
debug> watch('typeof item.shipping')
debug> c
break in cart.js:10
Watchers:
  0: sum = 0
  1: typeof item.shipping = 'number'

  8   let sum = 0;
  9   for (const item of items) {
>10     sum += item.price * item.qty + item.shipping;
 11   }
 12   return sum;
debug> c
break in cart.js:10
Watchers:
  0: sum = 53.99
  1: typeof item.shipping = 'string'

  8   let sum = 0;
  9   for (const item of items) {
>10     sum += item.price * item.qty + item.shipping;
 11   }
 12   return sum;
debug> exec item
{ name: 'cable', price: 9.5, qty: 2, shipping: '4.00' }
debug> .exit

The second stop shows the cause: typeof item.shipping is 'string'. The fix is Number(item.shipping).

Commands at the debug> prompt, from the command reference:

Command Action
c, n, s, o Continue, next line, step in, step out
sb(), sb(10), sb('fn()') Breakpoint on the current line, on line 10, on a function
sb('script.js', 1, 'num < 4') Conditional breakpoint in another file
bt Print the backtrace
watch('expr') Add a watch expression
repl Evaluate in the current scope (Ctrl+C to leave)
exec expr Evaluate one expression and print it

Pressing Enter repeats the previous command. The same client attaches to a running process: node inspect 127.0.0.1:9229 connects to an open inspector, and node inspect -p <pid> switches the inspector on in that process first. Both worked in the test runs.

An old tutorial that uses the node debug command predates all of this. That legacy debugger was deprecated in Node 7.7.0. On Node 26, node debug cart.js fails with Cannot find module, because debug is read as a file name.

Probe mode: print values without pausing

Node 26.1.0 added a second mode to node inspect. You name a line and an expression, Node runs the script, and you get the value at every hit. No pause, no console.log to remove afterwards:

$ node inspect --probe cart.js:10 --expr "sum" --probe cart.js:10 --expr "typeof item.shipping" cart.js
Hit 1 at file:///app/cart.js:10:17
  sum = 0
Hit 1 at file:///app/cart.js:10:17
  typeof item.shipping = "number"
Hit 2 at file:///app/cart.js:10:17
  sum = 53.99
Hit 2 at file:///app/cart.js:10:17
  typeof item.shipping = "string"
Completed

Add --cond to record only the hits you care about (Node 26.6.0 and later):

$ node inspect --probe cart.js:10 --expr "item" --cond "typeof item.shipping !== 'number'" cart.js
Hit 1 at file:///app/cart.js:10:17
  item = {name: "cable", price: 9.5, qty: 2, shipping: "4.00"}
Completed

--json prints the report as structured JSON for other tools to read (probe mode docs). Know the limits: the feature is experimental, it cannot attach to a running process, the session times out after 30 seconds unless you pass --timeout, and the script's own stdout and stderr are silenced.

WebStorm and other JetBrains IDEs

WebStorm, IntelliJ IDEA and the other JetBrains IDEs use the same inspector. From the WebStorm help (the page covers WebStorm 2026.3):

  1. Make sure the Node.js and JavaScript Debugger plugins are enabled.
  2. Go to Run | Edit Configurations, click Add and choose Node.js. Set the Node runtime and the main file, then click Debug.
  3. To attach to a running process, start it with --inspect or --inspect-brk and create an Attach to Node.js/Chrome configuration with the host and port.
  4. For nodemon or node --watch, tick Reconnect automatically in that configuration, so the debugger re-attaches after each restart.

Breakpoints in code: the debugger statement

Put debugger; anywhere in your code. When a client is attached, execution stops there. With no debugger, Node ignores the statement and keeps running.

app.post('/orders', async (req, res) => {
  const order = await createOrder(req.body);
  debugger; // pauses here when a debugger is attached
  res.json(order);
});

Remove it before you commit. ESLint's no-debugger rule is part of the recommended set, so most projects flag it automatically.

Debugging without a debugger: logs and environment variables

Plenty of nodejs debug sessions never need a breakpoint. Two environment variables switch on logging that is already in the code, and they are often confused.

NODE_DEBUG turns on debug output from Node's own core modules (CLI docs). Here is an HTTP request with NODE_DEBUG=http (output trimmed):

$ NODE_DEBUG=http node -e "require('node:http').get('http://example.com', r => { console.log(r.statusCode); r.resume(); })"
HTTP 10089: createConnection example.com:80:
HTTP 10089: sockets example.com:80: 1 1
HTTP 10089: outgoing message end.
(node:10089) Warning: Setting the NODE_DEBUG environment variable to 'http' can expose sensitive data (such as passwords, tokens and authentication headers) in the resulting log.

Take that warning seriously and keep NODE_DEBUG out of production logs. The same switch works for your own code through util.debuglog, which returns a no-op function when its section is not enabled:

// orders.js
const { debuglog } = require('node:util');
const log = debuglog('orders');

function createOrder(id) {
  log('creating order %d', id);
  return { id, status: 'new' };
}

createOrder(42);
console.log('done');
$ node orders.js
done
$ NODE_DEBUG=orders node orders.js
ORDERS 10087: creating order 42
done

DEBUG belongs to the debug package on npm (version 4.4.3 as of November 2026, MIT licensed), which Express and many other libraries use for their internal logs:

// npm install debug
const log = require('debug')('app:orders');
log('creating order %o', { id: 42 }); // printed only when DEBUG matches
$ DEBUG='app:*' node app.js
2026-11-10T07:05:29.262Z app:orders creating order { id: 42 }

Two more flags help in a terminal: --trace-warnings prints a stack trace for every process warning, and --stack-trace-limit=50 keeps more than the default 10 frames.

Async stack traces

You do not need a flag to see through await. V8 rebuilds the awaiting callers at no cost, so an error thrown three async calls deep still shows the whole chain:

$ node async.js
Error: user 7 not found
    at getUser (async.js:4:9)
    at async getOrder (async.js:8:16)
    at async handleRequest (async.js:13:10)

Per the V8 stack trace docs, this works for await, Promise.all() and Promise.any(). Outside those cases the async frames are not reconstructed.

npm debug command and npm debugger: what exists

npm has no debugger of its own. Typing the npm debug command people look for gives this on npm 11:

$ npm debug
Unknown command: "debug"

What you want depends on what is failing:

  • Your script, started through npm: use a JavaScript Debug Terminal, or put the flag in the script: "debug": "node --inspect src/server.js". Avoid NODE_OPTIONS=--inspect npm run dev, because npm is a Node program too and takes port 9229 first (see Troubleshooting).
  • npm itself, such as a failing install: run npm install --loglevel verbose. The default level is notice, and full debug logs go to the _logs directory inside the npm cache (npm logging docs).
  • The package named debug: that is the logging library above, not an npm debugger.

Debugging TypeScript and source maps

There are three ways to run TypeScript under a debugger. They differ in whether source maps are involved.

Route Command Source maps Use it when
Node's built-in type stripping node --inspect src/index.ts Not needed Your code uses only erasable syntax
tsx npx tsx --inspect-brk src/index.ts Nothing to set up You use enums, paths or other tsconfig.json features
Compile with tsc, run JavaScript node --inspect dist/index.js Required: "sourceMap": true You debug the same build you deploy
  1. v22.6.0Type stripping is added to Node, off by default.
  2. v23.6.0, v22.18.0Type stripping is enabled by default: node file.ts works with no flag.
  3. v25.2.0, v24.12.0Type stripping becomes stable. The opt-out flag is renamed to --no-strip-types.
  4. v26.0.0The --experimental-transform-types flag is removed. Syntax that needs code generation is left to third-party tools (TypeScript docs).

Built-in type stripping. Node replaces type syntax with whitespace and runs the file. Every line and column stays where it was, so the docs say source maps are unnecessary and Node does not generate them. That matches the test run: a debugger attached to node --inspect-brk cart.ts saw the script as cart.ts with no source map URL. Even node inspect cart.ts works, and shows the file with the types blanked out:

Break on start in cart.ts:2
  1
> 2 const cart         = [{ name: 'keyboard', price: 49.99, qty: 1 }];
  3 const total         = cart.reduce((sum, i) => sum + i.price * i.qty, 0);

The limits are strict. Node ignores tsconfig.json, does no type checking, and rejects .tsx files and TypeScript inside node_modules. Syntax that needs generated code fails: enums, namespaces with runtime code, parameter properties and import aliases.

$ node src/index.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode

tsx. For full TypeScript support the Node docs point to third-party runners, with tsx as the example (version 4.23.15 as of November 2026). With tsx the enum file runs, and an error it throws points at src/index.ts:13:21. In the test run, tsx --inspect-brk first paused inside tsx's own start-up code, so set a breakpoint in your file and press continue. The launch configuration from the tsx docs:

{
  "name": "tsx",
  "type": "node",
  "request": "launch",
  "program": "${file}",
  "runtimeExecutable": "tsx",
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen",
  "skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"]
}

Compiled output. Set "sourceMap": true in tsconfig.json, build, and debug the JavaScript. VS Code maps positions back to your .ts files. Point it at the build folder so it knows where to look:

{
  "name": "Launch compiled TypeScript",
  "type": "node",
  "request": "launch",
  "program": "${workspaceFolder}/src/index.ts",
  "outFiles": ["${workspaceFolder}/dist/**/*.js"]
}

Without source maps, a production stack trace points at compiled code. --enable-source-maps (stable since v15.11.0 and v14.18.0) fixes that:

$ node dist/index.js
Error: order 2 not found
    at findOrder (dist/index.js:9:15)

$ node --enable-source-maps dist/index.js
Error: order 2 not found
    at findOrder (src/index.ts:13:21)

Debugging tests: node:test, Vitest and Jest

Test runners spread work across processes or workers, and a debugger follows one at a time. Every runner has a switch that forces a single process. In VS Code, the shortcut for all three is the JavaScript Debug Terminal: run the test command in it and breakpoints in test files hit.

node:test. A test file is a normal script, so node --inspect-brk test/cart.test.js debugs one file directly. If you go through the runner, Node tells you what it changed:

$ node --inspect --test test/cart.test.js
(node:11245) Warning: Using the inspector with --test forces running at a concurrency of 1. Use the inspectPort option to run with concurrency

Narrow a run with --test-name-pattern="adds shipping". With --test-rerun-failures=state.json (v24.7.0 and later), the next run repeats only the tests that failed.

Vitest. The Vitest debugging guide (Vitest 5.1.0 is current as of November 2026) recommends --no-file-parallelism, plus --test-timeout=0 so tests do not time out while you sit on a breakpoint.

vitest --inspect-brk --no-file-parallelism

Its launch configuration for the file you have open:

{
  "type": "node",
  "request": "launch",
  "name": "Debug Current Test File",
  "autoAttachChildProcesses": true,
  "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
  "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
  "args": ["run", "${relativeFile}"],
  "smartStep": true,
  "console": "integratedTerminal"
}

Jest. The Jest troubleshooting page uses --runInBand, which runs every test in one process:

node --inspect-brk node_modules/.bin/jest --runInBand

On Windows the path is ./node_modules/jest/bin/jest.js. The launch configuration from the same page:

{
  "name": "Debug Jest Tests",
  "type": "node",
  "request": "launch",
  "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand"],
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen"
}

For a flaky test, a paused debugger can hide the timing that causes the failure. A logpoint or a hit count breakpoint on the suspicious line records what happened without stopping the run.

Remote and container debugging, safely

The debugging guide adds a point that is easy to miss: even on 127.0.0.1, every program on the same machine has unrestricted access to the port. Do not leave an inspector open on a shared server.

A remote server. Keep the inspector on localhost and tunnel to it, as the Node docs recommend:

# on the server
node --inspect app.js
# on your machine
ssh -L 9221:localhost:9229 user@remote.example.com
# then attach your debugger to localhost:9221

In VS Code, attach with "port": 9221, and add localRoot and remoteRoot so breakpoints map to the files on the server.

Docker. Inside a container, 127.0.0.1 is the container's own loopback, so a published port cannot reach an inspector bound to it. Bind to 0.0.0.0 inside the container and restrict the port on the host side instead:

services:
  api:
    build: .
    command: node --inspect=0.0.0.0:9229 src/server.js
    ports:
      - "3000:3000"
      - "127.0.0.1:9229:9229"

The 127.0.0.1: prefix matters. The Docker docs warn that a published port is reachable from outside the host unless you include the localhost address. Then attach, with remoteRoot set to your image's working directory, as in the VS Code container docs:

{
  "name": "Attach to Docker",
  "type": "node",
  "request": "attach",
  "address": "localhost",
  "port": 9229,
  "localRoot": "${workspaceFolder}",
  "remoteRoot": "/usr/src/app",
  "restart": true
}

Kubernetes. kubectl port-forward pod/my-api 9229:9229 forwards a local port to the pod and listens on localhost by default (kubectl reference).

Memory leaks: heap snapshots

A heap snapshot is a dump of every object the process holds. One snapshot tells you little. Two, taken before and after the leaking activity, show what was added in between. --heapsnapshot-signal writes one whenever the process receives a signal, with no debugger attached:

// leak.js
const http = require('node:http');
const cache = new Map(); // grows forever: nothing ever deletes entries

http.createServer((req, res) => {
  cache.set(`${Date.now()}-${Math.random()}`, Buffer.alloc(100_000).toString('hex'));
  res.end(`cached ${cache.size}\n`);
}).listen(3000, () => console.log('pid', process.pid));
$ node --heapsnapshot-signal=SIGUSR2 leak.js
pid 9617
$ kill -USR2 9617     # snapshot 1, after warm-up
$ # send 200 requests to http://localhost:3000
$ kill -USR2 9617     # snapshot 2
$ ls
Heap.20261110.095120.9617.0.001.heapsnapshot
Heap.20261110.095124.9617.0.002.heapsnapshot

Then follow the steps from the Node heap snapshot guide:

  1. Open Chrome DevTools and go to the Memory tab.
  2. Load the older snapshot file first and the newer one second.
  3. Select the newer snapshot and switch the dropdown at the top from Summary to Comparison.
  4. Look for large positive deltas, then explore the references that keep those objects alive in the bottom panel. In leak.js, the objects that pile up are the strings held by the cache map.

Other ways to get a snapshot: the Memory tab of an attached DevTools window, require('node:v8').writeHeapSnapshot(), or --heapsnapshot-near-heap-limit=3, which writes up to three snapshots as the process nears its heap limit. Use that one for "JavaScript heap out of memory" crashes you cannot reproduce on demand.

VS Code opens .heapprofile and .cpuprofile files in its built-in viewer but not .heapsnapshot files (profiling docs), so comparison work happens in Chrome.

Slow endpoints: CPU profiles

--cpu-prof starts V8's sampling profiler at start-up and writes a .cpuprofile file when the process exits. The flags have been stable since v22.4.0 and v20.16.0.

// hash.js
function slowHash(input) {
  let h = 0;
  for (let i = 0; i < 2e8; i++) h = (h * 31 + input.charCodeAt(i % input.length)) | 0;
  return h;
}

function parseConfig() {
  return JSON.parse('{"name":"devhunt","retries":3}');
}

const config = parseConfig();
console.log(slowHash(config.name));
$ node --cpu-prof --cpu-prof-dir=./profiles hash.js
1864774899
$ ls profiles
CPU.20261110.095049.9412.0.001.cpuprofile

Open the file in VS Code and you get a table sorted by self time. The file is plain JSON (nodes, samples, timeDeltas), so a few lines of Node can summarize it in a terminal or in CI:

// top.js: print the functions with the most self time in a .cpuprofile
const { readFileSync } = require('node:fs');
const profile = JSON.parse(readFileSync(process.argv[2], 'utf8'));
const nodes = new Map(profile.nodes.map((n) => [n.id, n]));
const self = new Map();
profile.samples.forEach((id, i) => {
  const { functionName, url, lineNumber } = nodes.get(id).callFrame;
  const key = `${functionName || '(anonymous)'} ${url.split('/').pop()}:${lineNumber + 1}`;
  self.set(key, (self.get(key) || 0) + profile.timeDeltas[i]);
});
const total = profile.timeDeltas.reduce((a, b) => a + b, 0);
[...self].sort((a, b) => b[1] - a[1]).slice(0, 5).forEach(([key, us]) => {
  console.log(`${(us / 1000).toFixed(1).padStart(8)} ms ${((100 * us) / total).toFixed(1).padStart(5)}%  ${key}`);
});
$ node top.js profiles/CPU.20261110.095049.9412.0.001.cpuprofile
  1462.5 ms  99.1%  slowHash hash.js:2
    10.1 ms   0.7%  (idle) :0
     1.4 ms   0.1%  compileFunctionForCJSLoader :0
     1.3 ms   0.1%  WritableState writable:303
     1.3 ms   0.1%  (anonymous) abort_controller:1

No guessing needed: 99.1% of the sampled time is inside slowHash.

A server never exits on its own, so for one slow endpoint it is easier to record on demand. Attach DevTools with node --inspect server.js, open the Performance panel, click Record, hit the endpoint a few times and stop. In VS Code, the Take Performance Profile button in the Call Stack view does the same.

Crashes and hangs: diagnostic reports

A diagnostic report is a JSON file with the JavaScript stack, the native stack, heap statistics, open handles and system limits at one moment. The docs describe it as intended for development, test and production use, and it is often the only evidence left after a crash.

$ node --report-uncaught-exception --report-directory=./reports crash.js
/app/crash.js:4
  return users[id].name.toUpperCase();
                   ^

TypeError: Cannot read properties of undefined (reading 'name')
    at loadUser (/app/crash.js:4:20)
    at Timeout._onTimeout (/app/crash.js:6:30)
    at listOnTimeout (node:internal/timers:685:17)
    at process.processTimers (node:internal/timers:618:7)

Writing Node.js report to file: report.20261110.095103.9536.0.001.json
Node.js report completed

Node.js v26.7.0

For a hang, or a script that finishes its work but never exits, start with --report-on-signal and send SIGUSR2. The libuv section lists what keeps the event loop alive. From a test script that left a timer and a server open (fields trimmed):

{"type":"timer","is_active":true,"is_referenced":true,"repeat":0,"firesInMsFromNow":28526}
{"type":"tcp","is_active":true,"is_referenced":true,"localEndpoint":{"host":"::","port":3000},"fd":14}

A referenced timer and a listening socket on port 3000: that is why the process stays up. Signal-based reports are not supported on Windows. There, call process.report.writeReport() from code.

Reports include every environment variable by default, so add --report-exclude-env (v23.3.0, v22.13.0) before you attach one to a ticket. --report-on-fatalerror covers crashes that never reach JavaScript, such as running out of memory.

Debugging production issues

Pausing a live process stops every request it is serving, so a breakpoint is the last resort in production. Start with logs, then files the process writes itself (diagnostic reports, CPU profiles, heap snapshots by signal), and only then an inspector switched on with SIGUSR1 and reached through an SSH tunnel.

Reliable tools for debugging production issues in a Python and Node.js stack

When services in two languages call each other, Node's built-in tools cover half of the request. These four work on both sides (facts from each vendor's site, November 2026):

Tool What it does Price or license Limitation
OpenTelemetry Vendor-neutral traces and metrics across services Open source, Apache-2.0 Logs are still in development for both languages; needs a backend
Sentry Error tracking with stack traces, plus tracing Free Developer plan (1 user, 5k errors); Team from $26 per month billed annually Captures the moment of the error, not live state
Lightrun Dynamic logs, snapshots and metrics added to running code Paid plans are "Custom"; a free plan exists for its AI SRE product Pricing is not public
Hud A runtime code sensor that records function-level behavior A "Start free" option; prices are not listed Works at function level, not for inspecting one variable on one line

For more options, browse the monitoring tools on DevHunt.

Lightrun vs Hud for Node.js debugging

Both run inside your Node process, but they answer different questions.

Lightrun Hud
What you get Dynamic logs, snapshots and metrics that you add to a line of running code Invocation counts, durations, exceptions and call relationships for every function
How you use it Add an action from the IDE or CLI while the app runs; no redeploy Install the SDK once; data flows to the IDE and to AI coding agents
Runtimes Java, Scala, Kotlin, Node.js, Python and .NET (docs) Node.js 16 and later, Python 3.8 and later, Java (FAQ)
Stated overhead Described as minimal 1 to 2 percent, per the FAQ

Pick Lightrun when you need the value of a variable on one line in production right now. Pick Hud when you need to know which function started failing or slowing down after a deploy. Neither replaces a local debugger for stepping through code.

Bun and Deno use the same idea

The flags carry over to the other runtimes, with one difference in protocol:

  • Deno speaks the same V8 inspector protocol. deno run --inspect-brk main.ts listens on 127.0.0.1:9229, and chrome://inspect works as it does for Node (Deno docs). See Deno on DevHunt.
  • Bun has --inspect, --inspect-brk and --inspect-wait too, but it speaks the WebKit Inspector Protocol instead. You use Bun's own web debugger at debug.bun.sh, or its VS Code extension, whose debugging support the Bun docs call experimental. See Bun on DevHunt.

If you are choosing between them, see Bun vs Deno.

Troubleshooting

What you see Cause Fix
Starting inspector on 127.0.0.1:9229 failed: address already in use Another process already holds port 9229 Find it with lsof -nP -iTCP:9229 -sTCP:LISTEN, or use --inspect=0 for a random port
The same message under NODE_OPTIONS=--inspect npm run dev npm is a Node process and takes 9229 first Put --inspect in the script, or use a JavaScript Debug Terminal
Breakpoints not hit, script finishes at once The code ran before the debugger attached Start with --inspect-brk or --inspect-wait
Gray hollow breakpoint, "Breakpoint ignored because generated code not found (source map problem?)" Source maps not loading: VS Code cannot map your .ts file to the running JavaScript Build with "sourceMap": true, set outFiles, then run Debug: Diagnose Breakpoint Problems
WebSockets request was expected You opened the inspector port in a browser tab, or used a host name Use chrome://inspect, and connect by IP address or localhost
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX Enum, namespace or parameter property under type stripping Run with tsx, or compile with tsc
Process will not exit, last line is Waiting for the debugger to disconnect... A client is still attached Close the DevTools window or stop the debug session

The port 9229 failure is quiet. The process prints the message and keeps running without an inspector:

$ NODE_OPTIONS=--inspect npm run dev
Debugger listening on ws://127.0.0.1:9229/fad44180-f451-439d-a33a-590b13b88fd6
For help, see: https://nodejs.org/learn/getting-started/debugging

> npmdemo@1.0.0 dev
> node -e "console.log('app started')"

Starting inspector on 127.0.0.1:9229 failed: address already in use
app started

The first "Debugger listening" line is npm, not your app.

Choosing your setup

Best for daily work on apps, scripts and tests: VS Code JavaScript Debug Terminal
No configuration. Run the command you already use and breakpoints in the editor hit, in JavaScript and TypeScript, for any Node process started from that terminal.
Best for memory leaks and slow endpoints: Chrome DevTools via `chrome://inspect`
The Memory tab compares heap snapshots and the Performance panel records CPU profiles. VS Code cannot open heap snapshots.
Best for servers, containers and CI: `node inspect`, probe mode and diagnostic reports
They are part of Node, need no GUI and work over SSH. Reports and --cpu-prof files can be collected first and read later.
Best for teams already on JetBrains IDEs: WebStorm
Same inspector underneath, with run configurations and automatic reconnect built in. Free only for non-commercial use.

If you are still learning Node's core APIs, Node.js readFile for beginners is a good next step, and a small file-reading script is a comfortable place to practice everything above.

How this guide was checked: on November 10, 2026, against the Node.js v26 docs and release schedule, the VS Code, WebStorm, Vitest, Jest, tsx, npm, Docker, kubectl, Deno and Bun docs, the ndb, node-inspector and js-debug repositories, and the Lightrun, Hud, Sentry and OpenTelemetry sites. Commands were run on Node v26.7.0 with npm 11.19.0 on macOS.

FAQ

Does Node.js have a built-in debugger?

Yes. node --inspect exposes the V8 inspector to VS Code, Chrome DevTools and WebStorm, and node inspect app.js starts a command-line debugger. Nothing needs to be installed.

Which Node.js command-line flag exposes the debugger interface?

--inspect. It activates the inspector on 127.0.0.1:9229 so a client can attach. --inspect-brk and --inspect-wait do the same and also hold your code until a debugger is connected.

How do I debug Node.js in VS Code?

Open the Command Palette, run Debug: JavaScript Debug Terminal and start your app in that terminal. Breakpoints set in the editor will hit. For a reusable setup, add a launch.json with a "type": "node" configuration.

How do I debug Node.js in Chrome?

Run node --inspect app.js, open chrome://inspect, and click inspect under Remote Target.

What is the difference between --inspect and --inspect-brk?

--inspect starts your code right away and lets a debugger attach at any time. --inspect-brk breaks at the start of your script, so you can set breakpoints before your code runs. --inspect-wait waits for a debugger to attach, then runs without pausing.

Is ndb or node-inspector still worth using?

No. node-inspector's last npm release was in January 2018, and ndb's repository is archived. Both were replaced by the built-in inspector, which VS Code and Chrome DevTools connect to directly.

Do I need a Node.js Chrome extension to debug?

No. Chrome and Edge attach to Node through the built-in chrome://inspect page. Third-party extensions such as NiM exist to manage DevTools sessions for you, but they are optional.

How do I debug nodejs code step by step?

Start with node --inspect-brk app.js and attach VS Code or Chrome, or run node inspect app.js in a terminal. Step with the toolbar buttons, or with n, s and o in the CLI.

How can I debug an npm install?

Run it with --loglevel verbose and read the full log in the _logs directory of the npm cache. There is no npm debug command. To debug a script that npm starts, debug the Node process itself.

Is node tree debugging the same as USB debugging?

No, and neither is about Node.js. "Enable node tree debugging" is a developer setting in Android's TalkBack screen reader that prints the accessibility node tree to the device log (TalkBack source). USB debugging is the Android developer option that lets adb talk to a device over USB (Android docs).