Debugging
🐞 Debugging
print() or console.log() statements — a real, common habit, and not a wrong one. But VS Code has a genuinely powerful visual debugger built in, often underused simply because it's less immediately obvious than adding a print statement. This chapter covers it end to end: configuring how your code runs, setting breakpoints, inspecting state, and stepping through execution.
▶️ The Run & Debug View
The Run & Debug icon in the Activity Bar (Chapter 1 — the play button with a bug) opens the debug panel. F5 starts a debug session; for many languages, VS Code can auto-detect how to run a simple script the first time, prompting you to create a proper configuration if it needs more information.
⚙️ launch.json — Configuring How to Run Your Code
For anything beyond a single trivial script, .vscode/launch.json tells the debugger exactly how to start your program:
type selects which debugger extension handles the session (installed as part of that language's extension, Chapter 4); request: "launch" means VS Code starts the program itself (as opposed to "attach", connecting to something already running); ${file} is a built-in variable meaning "whichever file is currently open."
🔴 Breakpoints
Clicking in the gutter — the empty space just left of a line number — places a red dot: a breakpoint. When the debugger reaches that line, execution pauses right before running it, with the program's entire current state available to inspect. Right-clicking a breakpoint offers Edit Breakpoint, letting you add a condition (e.g. only pause when i > 10) — useful inside a loop where you only care about one specific iteration, not every single pass.
🔬 Inspecting State When Paused
While paused, the debug panel's Variables section automatically lists every variable in scope — local and global — with their current values, no setup required. Hovering your mouse over any variable name directly in the editor shows its value in a small tooltip, right where you're already looking. The Watch section lets you add a specific expression (not just a bare variable — len(items) > 0 works too) that's continuously re-evaluated and displayed every time execution pauses.
📚 The Call Stack
The Call Stack panel shows the full chain of function calls that led to the current paused line — if main() called process_order() which called validate(), all three appear, in order. Clicking any frame in that list jumps the editor to that exact point, letting you inspect variables as they existed at each level of the call chain — genuinely useful for understanding how execution arrived somewhere, not just where it currently is.
👣 Stepping Through Code
Step Over (F10)
Runs the current line and moves to the next one — if it's a function call, runs the whole function without stepping into it.
Step Into (F11)
If the current line calls a function, jumps inside it, pausing at its first line.
Step Out (Shift+F11)
Finishes running the current function and pauses back where it was called from.
Continue (F5)
Resumes normal execution until the next breakpoint (or the program ends).
🔧 A Worked Example
Set a breakpoint on the return line, press F5. Execution pauses right before returning — the Variables panel shows total = 60, numbers = [10, 20, 30]. Hovering over total / len(numbers) in the editor (or adding it to Watch) shows 20.0 — the correct average, before the stray - 1 is applied. The bug is now visible directly, without ever adding a single print() line to the function.
The Debugger vs. print() Debugging
Print debugging requires guessing in advance what you'll need to see, editing the code to add a print statement, rerunning, and then remembering to remove it afterward — and every new question ("what about this other variable?") means repeating the whole cycle. The visual debugger inspects anything at the paused moment — no code changes, no reruns, and stepping through lets you follow the program's actual logic interactively rather than guessing where to place the next print statement.
Running a file normally (via the integrated terminal's python script.py, for instance, rather than pressing F5) completely ignores every breakpoint — they're a debugger-only feature, not something baked into the file itself. If breakpoints seem to be "not working," the first thing to check is whether the program was actually started via Run & Debug at all. For compiled or transpiled languages (TypeScript compiling to JavaScript, for example), breakpoints set in your original source also require source maps to correctly map back from the running compiled code — worth knowing this exists as a common source of "the breakpoint just doesn't hit" confusion in those setups.
💻 Coding Challenges
Exercise 1: Set Up launch.json and Hit a Breakpoint
In any small script, create a basic launch.json for your language, set a breakpoint on any line, and press F5. Confirm execution actually pauses there.
Exercise 2: Fix This Chapter's average() Bug
Using this chapter's worked example, set a breakpoint and use the debugger to confirm the - 1 is the bug, then fix the function so it returns the correct average.
Exercise 3: A Conditional Breakpoint in a Loop
Write a loop that runs at least 10 iterations. Set a conditional breakpoint inside it that only pauses on a specific iteration (e.g. when the loop variable equals 7), and confirm it doesn't pause on any earlier iteration.
🎯 What's Next
Next chapter: Snippets & Keybindings — writing custom snippets, remapping keys, and syncing settings across machines.