Stop Printing, Start Debugging
Debugging is the practice of pausing a running program at a specific line of code and inspecting its state — variable values, the call stack, environment variables — before deciding what happens next. It is the difference between guessing why your code is broken and knowing.
The simplest way to start debugging in VS Code is to press F5. For a basic script with no arguments, this works immediately. But Ops tooling is rarely that simple. Your script might need CLI flags (--dry-run, --env prod), environment variables (AWS_PROFILE, DB_PASSWORD), or a specific working directory. For these cases, you need a debug configuration.
Debug configurations live in .vscode/launch.json. This file tells VS Code exactly how to start your program for debugging. It is committed to your repository, so every team member gets the same debug setup.
Anatomy of launch.json
A debug configuration is a small JSON object naming which debugger to use, the entry-point file to run, and everything the process needs at startup — CLI arguments, environment variables, working directory. Each field has a specific purpose:
| Field | Purpose | Example |
|---|---|---|
name |
Label shown in the Run & Debug dropdown | "Debug Data Processor" |
type |
Which language debugger to use | "python", "node", "go" |
request |
launch starts a new process; attach connects to one already running |
"launch" |
program |
The entry point file to execute | "${workspaceFolder}/main.py" |
args |
CLI arguments passed to the script, as a list | ["--dry-run", "--verbose"] |
env |
Environment variables injected into the process | { "DB_HOST": "localhost" } |
cwd |
Working directory when the process starts | "${workspaceFolder}" |
The ${workspaceFolder} token is a VS Code variable that resolves to the root directory of your open project. Using it makes the config portable — it works on any machine regardless of where the repo is cloned.
Breakpoints: The Core Mechanic
A breakpoint is a marker you place on a line of code. When the debugger reaches that line, it pauses execution before running it. You can then:
- Hover over any variable to see its current value.
- Inspect the Variables panel on the left to see all variables in scope.
- Step Over (
F10): Execute the current line and pause on the next one. - Step Into (
F11): If the current line calls a function, jump inside that function. - Step Out (
Shift + F11): Finish the current function and return to the caller. - Continue (
F5): Resume running until the next breakpoint.
To set a breakpoint, click in the gutter — the narrow strip to the left of the line numbers. A red dot appears. That is all.
Multiple Configurations
A single launch.json can hold multiple configurations. This is useful for testing different scenarios without changing code — a "dry run against staging" entry and a separate, clearly-labeled "live run against prod" entry, each with its own arguments and AWS profile, sitting side by side in the same file. You select which configuration to run from the dropdown at the top of the Run & Debug panel. This pattern — multiple named configurations for different environments — is a clean alternative to maintaining separate shell scripts for each scenario.
A common interview question is: "How do you handle secrets in your local development workflow?" A weak answer is "I export them in my .bashrc." A strong answer acknowledges the risk of secrets in shell history and environment bleed between projects, then describes using launch.json with env fields (for non-sensitive dev values), a .env file loaded by envFile in launch.json (which is .gitignored), or a secrets manager integration. The envFile field in launch.json accepts a path to a .env file and loads it automatically — this keeps secrets out of the config file itself while still making them available to the debugger.