Debugging with --inspect

intermediate nodejs debugging devtools vscode

Console-log debugging works. Until it doesn’t. When we’re chasing a bug in async code with five awaits and a Promise.all, dropping breakpoints is way faster.

Node has a real debugger built in — same protocol Chrome DevTools uses. We just need to start Node with the right flag.

The two flags

  • --inspect — opens the debug port (default 127.0.0.1:9229). Code runs immediately.
  • --inspect-brk — same, but pauses on the very first line, waiting for a debugger to attach.
# Run normally with debugger available
node --inspect server.js

# Pause until DevTools attaches (good for debugging startup code)
node --inspect-brk server.js

In simple language: --inspect is “start running, I’ll attach whenever.” --inspect-brk is “wait for me before doing anything.”

We’ll see this in the terminal:

Debugger listening on ws://127.0.0.1:9229/abc-123
For help, see: https://nodejs.org/en/docs/inspector

Attach with Chrome DevTools

Open Chrome and go to chrome://inspect. Click Configure and make sure localhost:9229 is in the list. Our Node process shows up under “Remote Target” — click inspect.

You get full DevTools: Sources tab for breakpoints, Console for evaluating expressions in the current scope, Memory tab for heap snapshots, Performance tab for CPU profiles.

Attach with VS Code

This is the smoother workflow most of the time. Create .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug server",
      "program": "${workspaceFolder}/server.js",
      "skipFiles": ["<node_internals>/**"]
    },
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to running",
      "port": 9229
    }
  ]
}

Two modes:

  • Launch — VS Code starts Node with --inspect-brk itself. Hit F5, done.
  • Attach — we start Node with --inspect ourselves (e.g. inside Docker), then VS Code connects to port 9229.

Set breakpoints by clicking in the gutter. Hit them by triggering the code path (curl a route, run a script). Use the debug console to evaluate req.body or whatever in the paused scope.

--inspect vs --inspect-brk
--inspect
Code starts running.
Attach anytime.
Debug a live server.
--inspect-brk
Pauses on line 1.
Waits for attach.
Debug startup/init code.

Debugging inside Docker

The inspect port binds to 127.0.0.1 by default — won’t be reachable from outside the container. Bind to 0.0.0.0 and expose the port:

node --inspect=0.0.0.0:9229 server.js
# docker-compose.yml
services:
  app:
    ports:
      - "9229:9229"

Now VS Code attach config with "port": 9229 works against the container.

Warning: never expose 9229 in production. Anyone who can reach that port has remote code execution on our server.

Useful tricks

  • Conditional breakpoints — right-click a breakpoint, set a condition like userId === 42. Stops only when it matters.
  • Logpoints — instead of pausing, log a message. Same effect as console.log but without editing code.
  • debugger statement — drop the keyword debugger; in our code. If a debugger is attached, execution pauses there. If not, no-op.
  • --inspect with nodemonnodemon --inspect server.js gives auto-restart + debugger together.

When console.log is still fine

Honestly, for a quick “is this code path even running” question, console.log is faster. The breakpoint workflow shines for:

  • Inspecting complex object state at a point in time
  • Stepping through async/await flow
  • Catching an exception at the throw site (enable “Pause on caught exceptions”)
  • Debugging a heisenbug we can’t reliably reproduce