The debugger is the most underused tool in most developers' arsenal. A survey of engineering teams in 2025 found that over 60% of respondents reach for console.log or print before a debugger, even for complex multi-step bugs. The reason is familiarity, not effectiveness. A debugger gives you a live snapshot of the entire program state at the moment you care about — a print gives you whatever you thought to print before you knew what was wrong.
What changed in 2026
- VS Code debugger is now first-class for most languages. Node, Python, Go, Rust, Java, C++ — all work out of the box with minimal configuration. The days of "it's too hard to set up" are largely over.
- Time-travel debugging is production-ready. Tools like
rr (Linux), Replay.io (browser), and WinDbg's time-travel on Windows let you step backwards through execution — invaluable for heisenbug and race-condition debugging.
- AI-assisted debugging landed in IDEs. Copilot and Cursor can explain the current state at a breakpoint in natural language. Useful as a second opinion but not a replacement for understanding.
- Remote debugging over SSH is standard. VS Code Remote and JetBrains Gateway make attaching to a debugger on a remote server or container as easy as local debugging.
Core concepts
Breakpoint types
| Type |
What it does |
When to use |
| Line breakpoint |
Pause execution at this line |
Starting point for any bug |
| Conditional breakpoint |
Pause only when a condition is true |
Loops or repeated calls with specific inputs |
| Exception breakpoint |
Pause when an exception is thrown |
Catching errors before they propagate |
| Logpoint |
Log a value without stopping |
Low-intrusiveness observation in tight loops |
| Hit-count breakpoint |
Pause after N hits |
Finding the 100th iteration of a loop |
Stepping commands
- Step Over (F10) — Execute the current line and stop on the next one. Does not enter called functions.
- Step Into (F11) — Execute the current line AND enter any function called on that line.
- Step Out (Shift+F11) — Finish the current function and pause back in the caller.
- Continue (F5) — Resume until the next breakpoint.
Use Step Over to survey; use Step Into when you suspect the bug is inside a specific function.
Debugger setup: the essentials
Node.js / TypeScript in VS Code
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug API",
"program": "${workspaceFolder}/src/server.ts",
"runtimeArgs": ["--loader", "ts-node/esm"],
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"]
}
]
}
For an already-running process, use --inspect and attach:
node --inspect src/server.js # or --inspect-brk to pause at start
# Then Attach in VS Code or open chrome://inspect
Python with pdb / VS Code
# Quick in-code breakpoint (Python 3.7+)
breakpoint() # drops into pdb at runtime
# Or set in VS Code by clicking the gutter — no code change needed
# Run with pdb from the terminal
python -m pdb myscript.py
# Commands: n (next), s (step), c (continue), p varname, l (list), q (quit)
Java / JVM in IntelliJ
Set a breakpoint by clicking the gutter. Use "Evaluate Expression" (Alt+F8) to run arbitrary code at a breakpoint — this is equivalent to a print without modifying the source.
Working with the debugger: a workflow
- Reproduce the bug deterministically — a bug you cannot trigger reliably is very hard to debug with or without a debugger.
- Set a breakpoint at the highest-level point you own — just before the operation you suspect.
- Check the call stack — understand how you arrived here.
- Inspect variables in scope — look for nulls, wrong types, unexpected values.
- Step towards the error — Step Over until behaviour diverges from expectation, then Step Into that call.
- Use conditional breakpoints if the bug only appears for specific inputs.
// Example: only break when the order is for a large amount
// Condition: order.total > 10000 && order.currency === 'USD'
The Watch panel
Add expressions to the Watch panel to track them as you step:
- Variable:
user.profile.emailVerified
- Expression:
items.filter(i => i.status === 'pending').length
- Function call:
formatCurrency(rawAmount) (safe for pure functions)
This gives you a live dashboard without a single print statement.
Debugging async code
Async code is where the debugger earns its keep most decisively.
async function processPayment(orderId: string) {
const order = await fetchOrder(orderId); // breakpoint here
const result = await chargeCard(order); // then here
await updateOrderStatus(orderId, result); // then here
}
Set breakpoints inside the async function. The debugger pauses at each await in the expected order — something print statements cannot do cleanly across async boundaries.
For Node.js, enable "Just My Code" (skipFiles) to hide node_internals frames and focus on your own async chain.
Common mistakes
Setting breakpoints too deep. Start one level above where you think the bug is. You can always Step Into.
Ignoring the call stack panel. The call stack shows every active frame. Click a frame to inspect its local variables — this is often faster than stepping back to find where a bad value originated.
Not using conditional breakpoints in loops. A breakpoint inside a loop that runs 10,000 times will pause 10,000 times. Add a condition.
Leaving breakpoints in committed code. breakpoint() in Python or a debugger; statement in JavaScript will halt production if deployed. Lint rules can catch these: add no-debugger to ESLint and equivalent in ruff.
What to skip
- Print-driven debugging for async, multi-threaded, or time-sensitive code. The debugger is the right tool here.
- Remote debugging on production unless you have a read-only replica and strict access controls. Debug on staging.
- Debugging without a reproducible test case. Before attaching the debugger, write a failing test. The test documents the bug and becomes your regression guard.
FAQ
Is it worth learning the terminal debugger (pdb, gdb) if I have a GUI?
Yes, especially for server environments. When you are SSH-ed into a prod-like environment with no GUI, pdb or gdb is the only option.
Does the debugger work in Docker containers?
Yes. Expose the debug port in your docker-compose.yml and attach from VS Code Remote or your IDE. Most frameworks have a "debug mode" environment variable that enables the inspect port.
Can I debug a running process I did not start?
Yes — attach-to-process mode. In VS Code, use "Attach to Node Process." In Python, use pyrasite or debugpy's attach mode. In Java, start the JVM with -agentlib:jdwp.
What about debugging in production?
Distributed tracing (OpenTelemetry) and structured logging are your debugger in production. Avoid interactive debuggers in prod; use observability instead.
Where to go next