Debugging Techniques: What They Are and When to Use Each One
A practical map of the main debugging techniques — from print debugging to breakpoints, bisection, and production observability — and a clear guide to which one fits which kind of bug.
Debugging is a skill most engineers pick up by accident rather than by training, which is why the same senior engineer who writes elegant code can still spend three hours pattern-matching before finding a bug a junior would catch with the right technique in ten minutes. This is a map of the main techniques, and — more usefully — when each one actually applies.
Start with reproduction, not theories
Every technique below is wasted effort if you cannot reliably reproduce the bug first. Before reaching for any tool, nail down the exact steps, inputs, and environment that trigger the issue — an intermittent bug you cannot reproduce on demand needs a different strategy (usually logging or observability) than one you can trigger at will.
The core techniques
| Technique | Best for | Weakness |
|---|---|---|
| Print / log debugging | Quick checks, async flows, distributed systems | Noisy, requires guessing where to look |
| Breakpoints & step debugging | Complex local logic, unfamiliar code paths | Slow, doesn't scale to timing-sensitive bugs |
| Bisection (git bisect) | Regressions — "it used to work" | Needs a reliable repro and clean history |
| Rubber duck / explain-aloud | Logic errors, wrong assumptions | Does nothing for genuinely unknown behavior |
| Binary search on scope | Large, unfamiliar codebases | Slower than a direct breakpoint when you know where to look |
| Production observability (traces, RUM) | Bugs that only happen in prod / at scale | Requires instrumentation set up in advance |
Print / log debugging
Still the fastest tool for async code, race conditions, and anything spanning multiple processes or services, where a debugger cannot easily follow execution. The discipline that makes it effective: log inputs and outputs at boundaries, not implementation details, and remove the noise once the bug is found.
Breakpoints and step debugging
The right choice when you need to inspect real state at a specific moment — wrong values in a complex object, or an unfamiliar code path you need to walk line by line. Conditional breakpoints (stop only when a variable meets a condition) turn this from a blunt tool into a precise one for bugs buried in loops or high-frequency calls.
Bisection: finding when it broke
`git bisect` turns "somewhere in the last 200 commits" into a handful of binary-search steps against a known-good and known-bad commit. It is the single fastest technique for a genuine regression, provided you have a script or manual check that reliably confirms good vs. bad at each step.
Rubber duck debugging
Explaining the code line by line — to a colleague, or literally to an object on your desk — forces you to state assumptions out loud, which is usually where the bug is hiding. It costs nothing and catches a surprising share of logic errors before any tool is needed.
Production observability
Some bugs genuinely only happen at production scale, under real traffic patterns or with real data — they cannot be reproduced locally. Structured logging, distributed tracing, and real-user monitoring (RUM) are the only techniques that work here, which is why they need to be instrumented before the incident, not during it.
A decision guide
- Can you reproduce it reliably? If not, add logging/observability first — every other technique assumes you can trigger the bug on demand.
- Did it used to work? Bisect to find the exact change, then debug that diff specifically instead of the whole codebase.
- Is it a local logic error in code you understand? Rubber-duck it before reaching for a debugger — it is often faster.
- Is it in unfamiliar code, or does state matter? Use breakpoints, ideally conditional ones tied to the failing condition.
- Does it only happen in production? Instrument first — traces and structured logs, not a debugger, are the only tools that reach it.
The bottom line
No single technique is "the best" — the skill is matching the technique to the bug's shape: reproducibility, whether it is a regression, and whether it only exists at scale. This same systematic instinct — narrowing down root cause instead of guessing — is what we apply on every dedicated team engagement, especially in legacy modernization work where the "why" behind old behavior is rarely documented.
Frequently asked questions
What is the fastest way to debug a production-only bug?
Instrumentation you set up before the bug happens: structured logging with correlation IDs, distributed tracing, and real-user monitoring. Bugs that only occur under real production traffic or data usually cannot be reproduced locally, so the debugger is the wrong tool — observability data is what actually narrows it down.
Is print debugging unprofessional compared to using a real debugger?
No — it's the right tool for a specific class of bugs, particularly async and distributed flows where a step debugger struggles to follow execution across boundaries. Experienced engineers use both, chosen by the shape of the bug rather than by habit or perceived sophistication.
When should I reach for git bisect instead of debugging directly?
As soon as you know the code used to work and now does not, and you have (or can write) a reliable check for "is this commit good or bad." Bisection turns an open-ended search through history into roughly log2(n) steps, which is almost always faster than manually debugging an unfamiliar diff.