Ides Editors

Debugging Configuration Mastery in VS Code

A comprehensive guide to configuring VS Code's debugger for Node.js, including launch configs, attach mode, conditional breakpoints, logpoints, and multi-target debugging.

Debugging Configuration Mastery in VS Code

Most developers debug with console.log. It works, but it is the slowest path to understanding what your code actually does at runtime. VS Code's debugger lets you pause execution, inspect every variable, walk through the call stack, and evaluate expressions — all without modifying a single line of code. The difference in debugging speed is dramatic once you have launch configurations set up properly.

I configure debugging for every project I work on. The initial setup takes five minutes and saves hours of console.log-driven investigation. This guide covers every debugging configuration pattern that matters for Node.js development.

Prerequisites

  • VS Code installed
  • Node.js installed (v14+)
  • A project to debug
  • Basic understanding of breakpoints

Launch Configuration Basics

Debug configurations live in .vscode/launch.json. Create one with Ctrl+Shift+P → "Debug: Open launch.json" or click the gear icon in the Run and Debug sidebar.

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Program",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/src/index.js",
      "console": "integratedTerminal"
    }
  ]
}

The two request types:

  • launch — VS Code starts the process and attaches the debugger
  • attach — VS Code connects to an already-running process

Launch Configurations for Common Scenarios

Express/HTTP Server

{
  "name": "Launch Server",
  "type": "node",
  "request": "launch",
  "program": "${workspaceFolder}/app.js",
  "env": {
    "NODE_ENV": "development",
    "PORT": "3000",
    "DB_HOST": "localhost"
  },
  "envFile": "${workspaceFolder}/.env",
  "console": "integratedTerminal",
  "restart": true,
  "skipFiles": [
    "<node_internals>/**",
    "${workspaceFolder}/node_modules/**"
  ]
}

Key settings:

  • envFile loads your .env file automatically
  • restart: true restarts the debugger if the process crashes
  • skipFiles prevents the debugger from stepping into Node internals and dependencies

Running the Current File

{
  "name": "Run Current File",
  "type": "node",
  "request": "launch",
  "program": "${file}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

${file} resolves to whatever file is currently open and focused.

Jest Tests — Current File

{
  "name": "Debug Current Test",
  "type": "node",
  "request": "launch",
  "program": "${workspaceFolder}/node_modules/.bin/jest",
  "args": [
    "${relativeFile}",
    "--no-coverage",
    "--runInBand"
  ],
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen",
  "skipFiles": ["<node_internals>/**"]
}

--runInBand runs tests sequentially, which is necessary for the debugger to work correctly with breakpoints.

Jest — Specific Test by Name

{
  "name": "Debug Test by Name",
  "type": "node",
  "request": "launch",
  "program": "${workspaceFolder}/node_modules/.bin/jest",
  "args": [
    "${relativeFile}",
    "--no-coverage",
    "--runInBand",
    "--testNamePattern",
    "${selectedText}"
  ],
  "console": "integratedTerminal"
}

Select the test name in the editor, then run this config. Only the matching test executes.

npm Script

{
  "name": "Debug npm start",
  "type": "node",
  "request": "launch",
  "runtimeExecutable": "npm",
  "runtimeArgs": ["run", "dev"],
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

Script with Arguments

{
  "name": "Run CLI with Args",
  "type": "node",
  "request": "launch",
  "program": "${workspaceFolder}/bin/cli.js",
  "args": ["deploy", "staging", "--dry-run", "--verbose"],
  "console": "integratedTerminal"
}

Attach to Running Process

{
  "name": "Attach to Process",
  "type": "node",
  "request": "attach",
  "port": 9229,
  "restart": true,
  "skipFiles": ["<node_internals>/**"]
}

Start your app with the inspect flag:

node --inspect app.js
# or
node --inspect-brk app.js  # Break on first line

Attach to Docker Container

{
  "name": "Attach to Docker",
  "type": "node",
  "request": "attach",
  "port": 9229,
  "localRoot": "${workspaceFolder}",
  "remoteRoot": "/app",
  "restart": true,
  "skipFiles": ["<node_internals>/**"]
}

Docker Compose configuration:

services:
  app:
    command: node --inspect=0.0.0.0:9229 src/index.js
    ports:
      - "3000:3000"
      - "9229:9229"
    volumes:
      - .:/app

The localRoot/remoteRoot mapping tells VS Code how to translate file paths between your local machine and the container.

Breakpoint Types

Standard Breakpoints

Click the gutter (left of line numbers) to set a breakpoint. The red dot indicates the line where execution will pause.

Conditional Breakpoints

Right-click the gutter → "Add Conditional Breakpoint". The debugger only pauses when the condition is true:

// Expression examples:
user.role === "admin"
items.length > 100
err !== null
request.method === "POST" && request.path === "/api/users"
i % 1000 === 0    // Break every 1000th iteration

This is invaluable for debugging loops. Instead of stepping through 10,000 iterations, break only on the one that fails.

Hit Count Breakpoints

Right-click → "Add Conditional Breakpoint" → change dropdown to "Hit Count":

// Break on the 5th hit
5

// Break every 10th hit
10

// Break after 100 hits
>= 100

Logpoints

Logpoints print to the Debug Console without pausing execution. Right-click the gutter → "Add Logpoint":

// Log expression (use curly braces for expressions)
User {user.name} accessed {request.path} at {new Date().toISOString()}

// Log variable values
Processing item {i} of {items.length}: {JSON.stringify(item)}

// Performance timing
Request took {Date.now() - startTime}ms

Logpoints are console.log without modifying your code. They disappear when you close VS Code.

Exception Breakpoints

Configure in the Breakpoints panel:

  • All Exceptions — pause on every thrown exception (noisy but thorough)
  • Uncaught Exceptions — pause only on unhandled exceptions (recommended)

Toggle these in the Breakpoints panel at the bottom of the Run and Debug sidebar.

Inline Breakpoints

Place breakpoints at specific columns within a line. Useful for one-liners with multiple expressions:

// Set inline breakpoints at each function call
var result = transform(validate(parse(input)));

Shift+F9 adds an inline breakpoint at the cursor position.

Debug Console

The Debug Console (Ctrl+Shift+Y) is a REPL that runs in the context of the paused program. You can:

// Inspect variables
> user
{ id: 42, name: "Shane", role: "admin" }

// Evaluate expressions
> user.permissions.filter(p => p.startsWith("write"))
["write:articles", "write:users"]

// Call functions
> calculateTotal(cart.items)
299.97

// Modify variables (affects the running program)
> user.role = "superadmin"
"superadmin"

// Check types
> typeof response.data
"object"

// Complex inspection
> Object.keys(config).sort()
["database", "port", "secret", "timeout"]

Watch Expressions

Add expressions to the Watch panel that update as you step through code:

user.name
items.length
request.headers["authorization"]
process.memoryUsage().heapUsed / 1024 / 1024
err && err.message

Watches are evaluated at every step, giving you a live dashboard of values.

Call Stack Navigation

When paused, the Call Stack panel shows the function call chain. Click any frame to:

  • See the code at that point in the stack
  • Inspect local variables in that scope
  • Evaluate expressions in that context

This is how you trace where a bad value originated. Walk up the stack until you find where the variable was set incorrectly.

Multi-Target Debugging

Debug multiple processes simultaneously with compound configurations:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "API Server",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/api/server.js",
      "env": { "PORT": "3001" },
      "console": "integratedTerminal"
    },
    {
      "name": "Worker",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/worker/index.js",
      "console": "integratedTerminal"
    },
    {
      "name": "CLI Tool",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/bin/cli.js",
      "args": ["process", "--batch"],
      "console": "integratedTerminal"
    }
  ],
  "compounds": [
    {
      "name": "Full Stack",
      "configurations": ["API Server", "Worker"],
      "stopAll": true
    }
  ]
}

"stopAll": true stops all processes when you stop any one of them. The debug toolbar shows a dropdown to switch between active debug sessions.

Source Maps

When debugging transpiled code (TypeScript, Babel), source maps let you debug the original source:

{
  "name": "Debug TypeScript",
  "type": "node",
  "request": "launch",
  "program": "${workspaceFolder}/src/index.ts",
  "preLaunchTask": "tsc: build",
  "outFiles": ["${workspaceFolder}/dist/**/*.js"],
  "sourceMaps": true,
  "resolveSourceMapLocations": [
    "${workspaceFolder}/**",
    "!**/node_modules/**"
  ]
}

For ts-node (no separate compile step):

{
  "name": "Debug with ts-node",
  "type": "node",
  "request": "launch",
  "runtimeArgs": ["-r", "ts-node/register"],
  "args": ["${workspaceFolder}/src/index.ts"],
  "sourceMaps": true,
  "cwd": "${workspaceFolder}",
  "protocol": "inspector"
}

Pre-Launch Tasks

Run tasks before debugging starts:

{
  "name": "Build and Debug",
  "type": "node",
  "request": "launch",
  "program": "${workspaceFolder}/dist/index.js",
  "preLaunchTask": "npm: build",
  "postDebugTask": "cleanup"
}

Define the tasks in .vscode/tasks.json:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "npm: build",
      "type": "shell",
      "command": "npm run build",
      "problemMatcher": "$tsc"
    },
    {
      "label": "cleanup",
      "type": "shell",
      "command": "rm -rf dist/temp",
      "problemMatcher": []
    }
  ]
}

Complete Working Example: Full Debug Configuration

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Server",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/app.js",
      "envFile": "${workspaceFolder}/.env",
      "env": {
        "NODE_ENV": "development",
        "DEBUG": "app:*"
      },
      "console": "integratedTerminal",
      "restart": true,
      "skipFiles": [
        "<node_internals>/**",
        "${workspaceFolder}/node_modules/**"
      ],
      "autoAttachChildProcesses": true
    },
    {
      "name": "Test: Current File",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": [
        "${relativeFile}",
        "--no-coverage",
        "--runInBand",
        "--verbose"
      ],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "skipFiles": ["<node_internals>/**"]
    },
    {
      "name": "Test: Selected Name",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": [
        "${relativeFile}",
        "--no-coverage",
        "--runInBand",
        "--testNamePattern",
        "${selectedText}"
      ],
      "console": "integratedTerminal"
    },
    {
      "name": "Test: All",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": ["--no-coverage", "--runInBand"],
      "console": "integratedTerminal"
    },
    {
      "name": "Current File",
      "type": "node",
      "request": "launch",
      "program": "${file}",
      "console": "integratedTerminal",
      "skipFiles": ["<node_internals>/**"]
    },
    {
      "name": "Attach",
      "type": "node",
      "request": "attach",
      "port": 9229,
      "restart": true,
      "localRoot": "${workspaceFolder}",
      "skipFiles": ["<node_internals>/**"]
    }
  ],
  "compounds": [
    {
      "name": "Server + Attach",
      "configurations": ["Server"],
      "stopAll": true
    }
  ]
}

Auto-Attach

Instead of launch configurations, use auto-attach to debug any Node.js process started from VS Code's integrated terminal:

// User or workspace settings
{
  "debug.javascript.autoAttachFilter": "smart"
}

Auto-attach modes:

  • disabled — no auto-attach
  • smart — attach to scripts outside node_modules (recommended)
  • always — attach to every Node process
  • onlyWithFlag — only attach when --inspect is used

With smart auto-attach enabled, just run your script in the terminal:

node app.js          # Debugger attaches automatically
npm test             # Debugger attaches to test runner
node scripts/seed.js # Debugger attaches

Toggle auto-attach with Ctrl+Shift+P → "Debug: Toggle Auto Attach".

Common Issues and Troubleshooting

Breakpoints are grayed out ("Unverified")

The debugger cannot map the breakpoint to running code. Common causes: source maps are wrong, the file has not been loaded yet, or the path mapping is incorrect.

Fix: Check outFiles and sourceMapPathOverrides in your launch config. Ensure the file you set the breakpoint in is actually loaded by the program.

Debugger skips over breakpoints

Async code or optimized code can cause breakpoints to be skipped:

Fix: Use --nolazy flag to disable V8's lazy compilation:

{
  "runtimeArgs": ["--nolazy"]
}

Variables show as "undefined" in the debugger

V8 optimizes variables away in some cases:

Fix: Add --inspect-brk or set a breakpoint earlier. Avoid debugging production builds with minification enabled.

Cannot connect to Docker container debugger

The inspect address is bound to 127.0.0.1 inside the container:

Fix: Bind to 0.0.0.0 so it is accessible from outside the container:

node --inspect=0.0.0.0:9229 app.js

Test debugging is slow

Jest with --runInBand runs tests sequentially, which is slow for large suites:

Fix: Only debug the current file, not the entire suite. Use --testNamePattern to run a single test.

Best Practices

  • Use skipFiles to exclude node_modules and Node internals. Stepping into Express or lodash source code is rarely helpful. Skip it.
  • Prefer conditional breakpoints over manual stepping. Set a condition and let the program run at full speed until it hits the exact case you care about.
  • Use logpoints instead of console.log. They do not modify your code, do not need to be cleaned up, and can be toggled instantly.
  • Set up auto-attach for quick debugging. Smart auto-attach means you never need to think about launch configs for simple scripts.
  • Commit launch.json to version control. New team members should be able to press F5 and start debugging immediately.
  • Use the Debug Console as a REPL. Evaluate expressions, call functions, and modify variables while paused. It is far more powerful than logging.
  • Learn the keyboard shortcuts. F5 (continue), F10 (step over), F11 (step into), Shift+F11 (step out), Shift+F5 (stop). These should be muscle memory.

References

Powered by Contentful