Software Craft
Debugging Software Systematically
Debugging as a repeatable method, not luck — reproduce, isolate, form hypotheses, read the error, bisect, and the tools that make it faster.
Updated 2026-07-13 · 8 min read
The difference between someone who is good at debugging and someone who dreads it is not intelligence. It is method. Bad debugging is changing random things and hoping. Good debugging is a loop: form a theory about what is wrong, run an experiment that would prove it, and narrow the search each time.
A bug means your mental model of the program is wrong somewhere. The whole job is finding the exact point where reality diverges from what you believed. Do that patiently and almost any bug falls.
Reproduce it first
You cannot fix what you cannot see happen on demand. Before anything else, get the bug to occur reliably, ideally with the smallest possible set of steps.
A solid reproduction is worth more than any clever guess because it gives you a fast feedback loop. If you have to click through five screens and wait two minutes to see the bug, every experiment costs two minutes. Get it down to one command or one test.
- Write down the exact steps, inputs, and environment that trigger it.
- Strip away everything that is not required. Does it still happen with a smaller input? Without that plugin? On a fresh database?
- If it is intermittent, look for the hidden variable: timing, ordering, uninitialized memory, a shared cache, a race between two requests. "Random" bugs almost always have a cause you have not spotted yet.
The best possible reproduction is a failing automated test. If you can capture the bug as a test, you get a precise trigger now and protection against regressions forever.
Actually read the error
This sounds obvious and yet it is the step most often skipped. When something breaks, people glance at the first red line, feel a jolt of panic, and start guessing. The error message and stack trace usually contain most of the answer.
- Read the whole message slowly. It names the error type, often the exact value, and where it happened.
- Read the stack trace from the top, but find the topmost frame in your code — that is usually where to start looking, even if the crash surfaced deeper in a library.
- Watch for the classics: a
NullPointerException/ "undefined is not a function" means something you expected to exist did not. An off-by-one means a boundary is wrong. A type error means data is not the shape you assumed.
If the message is genuinely cryptic, paste the exact text into a search engine. You are almost never the first person to hit it. And if there is no error at all — just wrong output — your first job is to add one, by asserting what you expected and letting it fail loudly.
Isolate the problem
Once it reproduces, shrink the search space. The fastest way to find a bug in a thousand lines is to prove it is not in nine hundred of them.
