← Back to DevBytes

VS Code Debugging: Complete Guide

VS Code Debugging: Complete Guide

Debugging is one of the most critical skills for any developer, and Visual Studio Code ships with a powerful, built-in debugging system that rivals dedicated IDEs. Whether you are chasing a null reference in Node.js, a runtime error in Python, or a frontend bug in the browser, VS Code's debugger provides the tools you need to inspect, pause, and step through your code with precision. This guide walks you through everything from first principles to advanced workflows.

What Is VS Code Debugging?

At its core, VS Code debugging is an integration layer over the Debug Adapter Protocol (DAP). Instead of building separate debuggers for every language, VS Code communicates with language-specific debug adapters. This means the same UI, keyboard shortcuts, and workflows apply whether you are debugging JavaScript, Go, Java, or C++.

The debugger allows you to:

Why Debugging in VS Code Matters

While console.log and print statements have their place, they are slow, invasive, and require code changes. A proper debugger lets you inspect state without modifying your source, pause execution conditionally, and explore the entire runtime context. This dramatically reduces the time spent hunting for bugs, especially in complex applications with asynchronous flows or deep call stacks.

Key benefits include:

Getting Started: The Debug View

Open the Debug view by clicking the Run and Debug icon in the Activity Bar or pressing Ctrl+Shift+D (Windows/Linux) or Cmd+Shift+D (macOS). The first time you open it, VS Code will prompt you to create a launch configuration. You can also start debugging immediately by pressing F5, and VS Code will attempt to auto-detect your environment.

Launch Configurations Explained

Launch configurations live in a .vscode/launch.json file at the root of your project. This file defines one or more debugging scenarios. Here is a complete example for a Node.js project:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Current File",
      "program": "${file}",
      "console": "integratedTerminal",
      "skipFiles": ["/**"]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Server",
      "program": "${workspaceFolder}/src/server.js",
      "env": {
        "NODE_ENV": "development",
        "PORT": "3000"
      },
      "restart": true,
      "runtimeArgs": ["--inspect"]
    },
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to Process",
      "port": 9229,
      "address": "localhost"
    }
  ]
}

The two main request types are:

Breakpoints: Your Primary Tool

Breakpoints pause execution so you can inspect state. Click in the gutter next to a line number to set a standard breakpoint. VS Code also supports several advanced breakpoint types:

To set a conditional breakpoint, right-click the gutter and choose "Add Conditional Breakpoint." You can use either an expression or a hit count:

// Expression breakpoint — pauses only when i equals 50
i === 50

// Hit count breakpoint — pauses every 10th time
% 10

Stepping Through Code

Once execution is paused, use these essential keyboard shortcuts to navigate:

Inspecting Variables and the Call Stack

When paused, the Debug view shows the Variables panel with local, global, and closure-scoped variables. You can expand nested objects, hover over variables in the editor to see their values, and even edit values inline by double-clicking them.

The Call Stack panel shows the chain of function calls that led to the current pause point. Clicking any frame navigates the editor to that location, letting you inspect the state at each level of the stack.

The Watch panel lets you monitor specific expressions across the entire session. Add expressions like:

users.filter(u => u.active).length
request.body.email
this.state.counter

Debugging a Real Node.js Example

Let us debug a small Express application. Create src/server.js:

const express = require('express');
const app = express();
const PORT = 3000;

const users = [
  { id: 1, name: 'Alice', active: true },
  { id: 2, name: 'Bob', active: false },
  { id: 3, name: 'Charlie', active: true }
];

app.get('/users', (req, res) => {
  const activeOnly = req.query.active === 'true';
  const result = activeOnly ? users.filter(u => u.active) : users;
  res.json(result);
});

app.get('/users/:id', (req, res) => {
  const id = parseInt(req.params.id, 10);
  const user = users.find(u => u.id === id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Set a breakpoint on the line const result = activeOnly ? ... inside the /users handler. Launch the debugger with the "Debug Server" configuration from earlier. When you visit http://localhost:3000/users?active=true in your browser, execution pauses at the breakpoint. Inspect req.query, step over the filter, and examine result to verify the logic.

Debugging Frontend JavaScript in the Browser

VS Code can debug JavaScript running in Chrome or Edge using the built-in browser debugger. Install the Debugger for Chrome extension if you are on an older VS Code version, or use the built-in JavaScript debugger. Add this configuration:

{
  "type": "chrome",
  "request": "launch",
  "name": "Launch Chrome",
  "url": "http://localhost:8080",
  "webRoot": "${workspaceFolder}/public"
}

Start your dev server, then launch this configuration. VS Code opens a Chrome instance with debugging enabled. Set breakpoints in your frontend source files, and VS Code will pause execution directly in the editor — no need to switch to Chrome DevTools.

Debugging Python Applications

Install the official Python extension, then create a launch configuration:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Python: Current File",
      "type": "python",
      "request": "launch",
      "program": "${file}",
      "console": "integratedTerminal",
      "justMyCode": true
    },
    {
      "name": "Python: Flask",
      "type": "python",
      "request": "launch",
      "module": "flask",
      "env": {
        "FLASK_APP": "app.py",
        "FLASK_DEBUG": "1"
      },
      "args": ["run", "--no-debugger"],
      "jinja": true
    }
  ]
}

The justMyCode flag skips library code so you only step through your own source. Set justMyCode: false if you need to debug into third-party packages.

Attaching to a Running Process

Sometimes you cannot launch the process directly — for example, a Docker container or a production server. In these cases, use an attach configuration. For Node.js, start your process with inspection enabled:

node --inspect=0.0.0.0:9229 src/server.js

Then use this configuration:

{
  "type": "node",
  "request": "attach",
  "name": "Attach Remote",
  "address": "localhost",
  "port": 9229,
  "localRoot": "${workspaceFolder}",
  "remoteRoot": "/app"
}

For Docker, map port 9229 in your docker-compose.yml and use the same attach configuration. The localRoot and remoteRoot fields map source paths between your machine and the container.

Using the Debug Console

The Debug Console lets you evaluate expressions in the context of the paused execution. This is invaluable for testing hypotheses without modifying code. For example, while paused in the Express handler above, you could type:

users.map(u => u.name)
req.query.active
users.find(u => u.id === 2)

The console supports autocomplete based on the current scope, making it easy to explore available variables and methods.

Compound Configurations

Full-stack applications often require debugging both the backend and frontend simultaneously. Compound configurations launch multiple debuggers at once:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Backend",
      "program": "${workspaceFolder}/server/index.js"
    },
    {
      "type": "chrome",
      "request": "launch",
      "name": "Frontend",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}/client"
    }
  ],
  "compounds": [
    {
      "name": "Full Stack",
      "configurations": ["Backend", "Frontend"]
    }
  ]
}

Select "Full Stack" from the dropdown and press F5 to start both sessions. The call stack panel shows both debug targets, and you can switch between them seamlessly.

Best Practices

Debugging Tests

VS Code makes it easy to debug individual test files. For Jest, add this configuration:

{
  "type": "node",
  "request": "launch",
  "name": "Debug Jest Tests",
  "program": "${workspaceFolder}/node_modules/.bin/jest",
  "args": ["--runInBand", "${fileBasename}"],
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen"
}

Open a test file, set breakpoints in either the test or the code under test, and launch this configuration. The debugger runs only the current file's tests and pauses at your breakpoints.

Troubleshooting Common Issues

Conclusion

Mastering VS Code's debugger transforms how you approach bugs. Instead of guessing and scattering print statements, you gain surgical control over execution, real-time visibility into state, and the ability to test hypotheses instantly through the Debug Console. Start with simple launch configurations, learn the core keyboard shortcuts, and gradually incorporate advanced features like conditional breakpoints, compound configurations, and remote attaching. The investment pays off every single day — fewer bugs ship, debugging sessions get shorter, and your understanding of your own code deepens with every pause and step.

— Ad —

Google AdSense will appear here after approval

← Back to all articles