Debugging Techniques That Actually Work
Most debugging time is lost to guessing. A few systematic techniques eliminate the guesswork and find bugs faster, regardless of the language or system you're working in.
Published April 22, 2026The experience of debugging badly is familiar: you change something, run the code, observe no change, change something else, and repeat. An hour later you're staring at the same error and have learned almost nothing. The techniques below don't eliminate bugs, but they replace aimless mutation with directed investigation.
Build a mental model first, then check it
Before changing any code, write down (or say aloud) what you think the bug is. A specific hypothesis: not "something's wrong with the login flow" but "I think the session token is not being set correctly after the redirect." A vague theory produces vague experiments. A specific hypothesis produces a specific test.
Once you have a hypothesis, design the cheapest possible test that would prove it wrong. If your hypothesis is wrong, you haven't lost much time. If it survives, you can refine it or act on it.
Read the full error message
This sounds obvious, but most developers read the first line of an error message and stop. The useful information is almost always somewhere else: in the stack trace, in a "caused by" clause, or in the line that says exactly which file and line number triggered the exception. For database errors, the raw SQL the driver generated (often logged separately) is more useful than the exception class name.
Copy the exact error message — word for word, with the error code if one exists — before searching for it. Paraphrasing the error produces worse search results.
Reproduce it first, minimize it second
If you can't reproduce the bug reliably, you can't tell when you've fixed it. Before investigating why, invest in being able to produce the bug on demand. Write a failing test that captures the exact inputs and expected output. If the bug is intermittent, instrument the code to capture the state when it next occurs rather than trying to produce it synthetically.
Once you can reproduce it, reduce the reproduction to the smallest possible case. Strip away unrelated code, configuration, and data until the bug still appears but everything inessential is gone. Minimizing often reveals the cause directly.
Binary search the codebase
When you know a bug exists somewhere in a large block of code, use the binary search principle: find the midpoint, test whether the bug still occurs, and eliminate the half that doesn't contain it. Repeat until you've isolated the exact location.
In version control, this is git bisect: you mark a known-good commit and a known-bad commit, and Git checks out the midpoint for you to test. It takes O(log n) steps to isolate the commit that introduced the bug across n commits.
git bisect start
git bisect bad HEAD # current commit is broken
git bisect good v2.1.0 # this tag was fine
# Git checks out the midpoint; test your code
git bisect good # or:
git bisect bad
# repeat until git prints the culprit commit
Use a real debugger, not just print statements
Print statements have their place (quick sanity checks, logging in production), but an interactive debugger is dramatically more powerful for complex bugs. You can inspect any variable at any point in the call stack, step through code line by line, and set conditional breakpoints that fire only when a specific condition is met.
In Python, breakpoint() (or import pdb; pdb.set_trace()) drops you into an interactive session mid-execution. In JavaScript, debugger; in your code pauses execution in browser DevTools. Most languages have equivalent tools.
The key debugger skill is not setting breakpoints at where you think the bug is, but at where you're confident the code is still correct, then stepping forward until it goes wrong.
Rubber duck debugging
Explaining a problem forces you to make assumptions explicit. Tell a rubber duck (or a patient colleague) what the code is supposed to do, what it's actually doing, and why you think those differ. The act of articulating the gap frequently reveals the bug before you finish the sentence — because you notice mid-explanation that you've been assuming something that you haven't actually verified.
Check what changed recently
Most bugs are introduced by changes. If a thing worked yesterday and doesn't today, look at what changed between yesterday and today: new code, dependency updates, configuration changes, data migrations, infrastructure changes. git log --since="1 day ago" --oneline is often the fastest route to the culprit.
Distinguish bugs from misunderstandings
Some "bugs" are the code doing exactly what it was written to do, but what it was written to do is wrong. Others are the code failing to do what it was written to do. Before investigating the implementation, verify that your expectations are correct: check the documentation, re-read the spec, test with a simpler input. You may find the bug is in your mental model, not the code.
Debugging is a skill that improves with practice, but the bottleneck is almost never tool knowledge — it's the discipline to resist guessing and instead gather evidence before drawing conclusions.