Debugging in VS Code
VS Code's built-in debugger lets you set breakpoints, inspect memory, change variable values, and step through execution — all without leaving your editor or adding console.log.
Let's be honest: most of us debug with console.log. It works. But it has a ceiling. You have to predict which values to log, re-run to see them, add more logs when you were wrong, and clean them up when you're done.
VS Code's debugger removes that ceiling. You freeze the world at any line, inspect every variable in memory, change values on the fly, and step through the execution path — all without editing the file or re-running the program.
launch.json: Connecting VS Code to a Debugger
Debugger configuration lives in .vscode/launch.json. To create one:
Run and Debug panel (the play button with a bug icon in the sidebar) → "create a launch.json file" → choose a debugger type.
Or: Command Palette → "Debug: Open launch.json."
Node.js (most common)
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Server",
"program": "${workspaceFolder}/src/server.js",
"skipFiles": ["<node_internals>/**"]
}
]
}type: the debugger —node,chrome,msedge, or a custom one from an extensionrequest:launch(start a process) orattach(connect to a running one)program: the entry point to runskipFiles: don't step into these (node internals, node_modules)
Hit F5 to start debugging with the active configuration.
Attaching to a Running Process
If your app is already running (maybe started by a Task), attach to it:
{
"type": "node",
"request": "attach",
"name": "Attach to Running Node",
"port": 9229
}Start your app with node --inspect src/server.js to enable the inspector port. Then attach from VS Code.
Browser Debugging (Chrome/Edge)
{
"type": "chrome",
"request": "launch",
"name": "Debug in Chrome",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src"
}Launches Chrome and connects VS Code's debugger to it. Breakpoints in your TypeScript/JSX source files map to the running browser app.
Chrome DevTools are excellent — use whichever feels more natural. The VS Code advantage: your breakpoints are next to your code, not in a separate browser window.
Breakpoints
Click the gutter (left of the line number) to set a breakpoint. A red dot appears. When execution reaches that line, it stops.
F5 → start / continue
F10 → step over (go to next line, don't enter functions)
F11 → step into (enter the function on this line)
Shift+F11 → step out (finish current function, return to caller)
Shift+F5 → stop debuggingWhen stopped, you can:
- Hover over any variable to see its current value
- Open the Variables panel (sidebar) to see everything in scope
- Open the Call Stack panel to see how you got here
Watching Values
In the Watch panel, add expressions to evaluate continuously as you step through:
request.user.id
items.length
isAuthenticatedThese update every time execution pauses. More reliable than repeating hovers.
Changing Variables at Runtime
This is the most underused feature. When paused at a breakpoint, you can change a variable's value in the Variables panel — right-click → "Set Value."
The use case: you're debugging a function that takes 5 clicks to reach. When you finally hit the breakpoint, the value is wrong. Instead of going back and clicking again with a different input, change the value right here and continue. Does the rest of the code path work with the corrected value? You know in 5 seconds.
Pause at breakpoint
→ Variables panel → right-click variable → "Set Value"
→ type new value
→ F5 to continueConditional Breakpoints
A regular breakpoint stops every time. A conditional breakpoint stops only when an expression is true.
Right-click the breakpoint dot → "Edit Breakpoint" → set a condition:
userId === undefined
items.length > 100
request.method === "DELETE"The use case: you're iterating over 500 items and the bug only happens on one of them. Without a conditional breakpoint, you'd hit F5 499 times. With one, execution stops exactly when the bad case occurs.
Hit Count Breakpoints
Also in the Edit Breakpoint menu: stop after N hits. Useful when you know the bug happens "sometimes" but you suspect it's the 3rd or 7th iteration:
Hit Count: >= 10 → stop after the 10th time this line runsNavigating the Call Stack
When stopped, the Call Stack panel shows every function call that led to this point.
Click any frame in the stack to jump to that function and see the local variables at that point. You can navigate the entire call history — going up into Express middleware, into your router, into your handler — without leaving VS Code and without adding logging.
This is where the advantage over console.log becomes clear. With logging, you add a log, re-run, add another log higher up, re-run. With the debugger, you pause once and navigate the entire stack in seconds.
Break on Exceptions
Instead of setting specific breakpoints, you can tell VS Code to stop whenever an exception occurs:
Breakpoints panel (in the Run and Debug sidebar) → check "Caught Exceptions" or "Uncaught Exceptions."
Uncaught Exceptions (the useful default): stop when an error isn't caught anywhere. You see exactly where it happened with the full stack.
Caught Exceptions: stops even when a try/catch handles it. This catches everything — including expected errors — so use it carefully.
Multi-Root Workspaces
If your frontend and backend live in separate folders, you can treat them as a single VS Code workspace:
File → Add Folder to Workspace... → select the second project folder.
Or create a .code-workspace file:
{
"folders": [
{ "path": "./api" },
{ "path": "./frontend" }
],
"settings": {
"editor.formatOnSave": true
}
}Open the .code-workspace file in VS Code and both folders appear as a single workspace. Shared settings apply to both. Tasks and launch configurations can span folders.
Honest take: if you can put both in a monorepo, do that instead. Multi-root workspaces solve a real problem — jumping between two repos that belong together — but a monorepo solves it more cleanly. If you're inheriting a setup with multiple repos, multi-root workspaces are the best path forward.
The Practical Workflow
1. Set a breakpoint near where things go wrong
2. F5 to start (or attach to running process)
3. Trigger the behavior
4. When paused:
- Check variable values in the panel or by hovering
- Navigate the call stack to find where the bad value came from
- Change a variable to test if the rest of the path is correct
5. F10 to step through line by line
6. F5 to continue to the next breakpoint
7. When done: Shift+F5 to stopThe first time you set up a launch.json, it takes 5 minutes. After that, F5 is your new debugging workflow. The console.log habit doesn't disappear overnight — but every time you find yourself adding a log, removing it, adding one somewhere else, consider that you could have just stopped at a breakpoint and seen everything at once.
Keep reading
Enjoyed this? Get more like it.
Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.