Debugging Node.js in VS Code
Visual Studio Code ships with a first-class debugger for Node.js, so you can set breakpoints, step through code, and inspect live variables without ever reaching for a console.log. The behaviour is driven entirely by configuration in a launch.json file, which describes how VS Code should either launch your program under the debugger or attach to an already-running process. Mastering these configurations turns the editor into an interactive lab for understanding exactly what your code does at runtime.
Creating a launch.json
A launch.json file lives in a .vscode folder at the root of your project and holds an array of debug configurations. The fastest way to create one is to open the Run and Debug view (Ctrl+Shift+D / Cmd+Shift+D), click create a launch.json file, and choose Node.js. VS Code scaffolds a starter configuration that you can then edit by hand.
A minimal launch configuration looks like this:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch app",
"program": "${workspaceFolder}/src/index.js",
"skipFiles": ["<node_internals>/**"]
}
]
}
The ${workspaceFolder} variable resolves to your project root, and skipFiles keeps the debugger from stepping into Node’s internal modules so you stay focused on your own code.
Tip: If your entry point is defined as an npm script, set
"runtimeExecutable": "npm"and"runtimeArgs": ["run", "dev"]instead ofprogram. This reuses the exact start command your team already runs.
Launch vs attach
Every configuration has a request field set to either launch or attach. The difference matters: a launch config starts a brand-new Node process under the debugger, while an attach config connects to a process that is already running with the inspector enabled. Use launch for local development and attach when the process is started by something else — a Docker container, a watcher, or a remote host.
| Aspect | launch | attach |
|---|---|---|
| Process lifecycle | VS Code starts and owns it | Already running independently |
| Key fields | program, args, env | port, address, processId |
| Typical use | Local app development | Containers, remote, long-running servers |
| Restart behaviour | Stops process on disconnect | Process keeps running after detach |
To attach, the target must be started with the inspector flag, for example:
node --inspect=9229 src/index.js
Then point an attach configuration at that port:
{
"type": "node",
"request": "attach",
"name": "Attach to 9229",
"port": 9229,
"restart": true,
"skipFiles": ["<node_internals>/**"]
}
The restart: true option tells VS Code to automatically re-attach if the process restarts, which pairs well with a nodemon-style watcher.
Setting breakpoints
A breakpoint pauses execution at a specific line so you can inspect the call stack, scopes, and variable values. Click in the gutter to the left of any line number, or place your cursor on a line and press F9. When the debugger hits that line, execution freezes and the editor highlights it.
import { readFile } from "node:fs/promises";
async function loadConfig(path) {
const raw = await readFile(path, "utf8"); // set a breakpoint here
const config = JSON.parse(raw);
return config;
}
const config = await loadConfig("./config.json");
console.log(config.port);
While paused, the toolbar exposes Continue (F5), Step Over (F10), Step Into (F11), and Step Out (Shift+F11). The Variables panel shows everything in scope, and you can hover over any identifier in the editor to see its current value.
Conditional breakpoints
A plain breakpoint stops every time a line runs, which is painful inside loops. A conditional breakpoint only pauses when a JavaScript expression evaluates to true. Right-click the gutter and choose Add Conditional Breakpoint, then enter an expression.
const users = await fetchUsers();
for (const user of users) {
// Conditional breakpoint condition: user.id === 42
await sendWelcomeEmail(user);
}
VS Code offers three variants from the same menu:
| Type | Pauses when | Use case |
|---|---|---|
| Expression | Condition is truthy | Stop on a specific record or state |
| Hit Count | The line has run N times | Skip the first N iterations |
| Logpoint | Never — logs a message instead | Trace without editing source |
A logpoint is especially handy: enter a message like processing {user.id} and VS Code prints it to the Debug Console each time the line runs, with {} expressions interpolated, all without pausing or adding console.log calls to your code.
The debug console
While the program is paused, the Debug Console (Ctrl+Shift+Y) is a live REPL evaluated in the current stack frame. You can read variables, call functions, and even mutate state to test hypotheses.
> user.roles
[ 'editor' ]
> user.roles.includes('admin')
false
> config.port * 2
6000
Anything you type runs in the paused context, so await works for promises and you have full access to closures that are otherwise invisible. The console also surfaces your program’s own console.log output, color-coded by stream.
Auto-attach
Auto-attach lets VS Code attach the debugger automatically to any Node process you start from the integrated terminal — no launch.json required. Open the Command Palette (Ctrl+Shift+P), run Debug: Toggle Auto Attach, and choose a mode.
| Mode | Behaviour |
|---|---|
smart | Attaches to scripts outside node_modules (recommended) |
always | Attaches to every Node process in the terminal |
onlyWithFlag | Attaches only when --inspect is passed |
disabled | Turns the feature off |
With auto-attach on, simply running node src/index.js or npm test in the terminal will hit your breakpoints. Under the hood VS Code injects the inspector flag for you, so there is nothing extra to configure.
Warning:
alwaysmode can attach to short-lived helper processes and slow down command-line tooling.smartmode is almost always the better default because it ignores dependency scripts.
Best Practices
- Keep a small set of named configurations in
launch.json(launch, attach, test) so any teammate can debug with one click. - Always include
"skipFiles": ["<node_internals>/**"]to avoid stepping into Node core during normal debugging. - Prefer logpoints over temporary
console.logstatements — they leave your source untouched and disappear when you remove them. - Use conditional breakpoints in loops and event handlers instead of stopping on every iteration.
- For TypeScript or transpiled code, enable source maps in your build and set
"sourceMaps": trueso breakpoints map to the original files. - Set auto-attach to
smartfor friction-free terminal debugging without maintaining configs. - When attaching to containers or remote hosts, expose the inspector on
0.0.0.0:9229carefully and never leave it open in production.