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:
- Set breakpoints to pause execution at specific lines
- Inspect variables and the call stack at runtime
- Step through code line by line
- Watch expressions and modify values on the fly
- Attach to already-running processes
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:
- Non-invasive inspection: No need to litter your code with logging statements.
- Conditional control: Pause only when specific conditions are met.
- State exploration: Drill into nested objects and modify values live.
- Multi-language support: One consistent interface across your stack.
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:
- launch — VS Code starts the program and attaches the debugger.
- attach — VS Code connects to an already-running process.
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:
- Conditional breakpoints: Pause only when an expression evaluates to true.
- Logpoints: Log a message without pausing — a cleaner alternative to
console.log. - Function breakpoints: Break when a specific function is called, even without knowing its location.
- Exception breakpoints: Pause when exceptions are thrown.
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:
- Continue (F5): Resume execution until the next breakpoint.
- Step Over (F10): Execute the current line without descending into functions.
- Step Into (F11): Descend into the function called on the current line.
- Step Out (Shift+F11): Execute the rest of the current function and pause at the caller.
- Restart (Ctrl+Shift+F5): Restart the debugging session.
- Stop (Shift+F5): End the session.
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
- Commit your launch.json: Share debugging configurations with your team by committing
.vscode/launch.jsonto version control. - Use Logpoints for production-like debugging: They inject logs without modifying source files, which is ideal for shared environments.
- Leverage conditional breakpoints: Avoid pausing on every iteration of a loop — use conditions to target specific states.
- Learn the keyboard shortcuts: Muscle memory for stepping and continuing will save hours over time.
- Use
skipFilesto ignore noise: Skip framework internals and node built-ins to focus on your code. - Debug tests, not just apps: Most test frameworks integrate with the VS Code debugger, letting you step through failing tests.
- Keep configurations focused: Create separate configurations for different scenarios rather than one overly complex setup.
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
- Breakpoints not hitting: Ensure source maps are enabled and paths are correct. Check
webRootfor frontend debugging. - "Cannot connect to runtime": Verify the process was started with
--inspectand the port is accessible. - Stepping into library code: Add patterns to
skipFilesto avoid descending into dependencies. - Slow debugging: Disable heavy watch expressions and reduce the number of active breakpoints.
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.