Debugging is investigation
Debugging is the process of finding why a system behaves differently from what was intended, and then fixing it. The most effective debuggers do not rely on inspiration. They treat the work as an investigation: gather evidence, form a hypothesis, test it, and repeat. The same approach applies to software, to embedded systems and to complex systems made of several parts.
A calm routine matters because bugs tend to arrive when time is short. The steps below are not strict, but following them keeps you from guessing.
Reproduce the bug
You cannot reliably fix what you cannot reproduce. Start by finding a way to trigger the fault on demand.
- Record the exact steps, inputs, settings and environment that lead to the failure.
- Note what the system did and what it should have done.
- Try to shrink the case. Remove steps, data and features until the smallest sequence that still fails remains.
- If the bug is intermittent, look for what differs between failing and passing runs: timing, load, data, configuration or hardware.
A short, repeatable test case is valuable. It speeds up every later experiment, and it can become an automated test that guards against the bug returning. Writing it up well also produces a useful bug report for others.
Read the evidence first
Before changing anything, look at what the system is already telling you.
- Read the full error message and stack trace, not just the last line.
- Check logs around the time of failure.
- Look at the inputs and state when it failed.
- Ask what changed recently: code, configuration, dependencies, data or environment.
Many bugs are explained by the first careful reading of a message that was glanced at and dismissed.
Isolate the cause
Isolation means narrowing down where the fault lives. Several techniques help.
Divide and conquer. If a process has many stages, check the data in the middle. If it is correct there, the fault is later; if not, earlier. Repeat on the remaining half. This binary search finds a fault among many steps surprisingly quickly: eight halvings are enough to find one among 256 stages, since 2 to the power 8 is 256.
Bisecting changes. If the bug appeared between two known versions, version control can help you find the change that introduced it. You test a version halfway between the last good and first bad, then halve the range again. Some tools automate this. It works best when commits are small and the test is quick, which is one reason to follow good habits with commit messages and git basics.
Remove and replace. Disable parts of the system, or swap in a known-good component or a simple stand-in, to see whether the problem persists.
Compare working and broken cases. Run the same input on a working setup and a broken one and look at where they diverge.
Check boundaries. Faults often occur at interfaces, where one component’s assumptions meet another’s. The idea is developed in system interfaces.
Use logging and observation wisely
When you cannot see inside the system, add ways to look.
- Logging. Record key values, decisions and events with timestamps. Useful messages say what happened and include the relevant data, not just “here”.
- Levels. Use levels such as debug, info and error so that you can turn detail up when needed without drowning in output.
- Debuggers. An interactive debugger lets you pause the program, inspect variables and step through code. It is excellent for logic errors that you can reproduce.
- Assertions. Checks that state your assumptions in code, failing loudly when they are wrong, move the point of detection closer to the cause.
- Measurement tools. For hardware and systems, instruments and monitoring serve the same purpose.
Remember that observing can change behaviour. Heavy logging can alter timing and hide, or create, race conditions. If a bug disappears when you add a print statement, timing is a likely suspect.
Form and test hypotheses
At each stage, write down your current best explanation and the test that would disprove it.
- List possible causes that fit the evidence.
- Rank them by likelihood and by how cheap they are to check.
- Predict what you would see if the explanation were true.
- Run the smallest experiment that tests the prediction.
- Change one thing at a time. If you change three things and the bug goes away, you do not know which one mattered.
Keep notes of what you tried and what you found, including negative results. They prevent repeated work and are valuable if you must hand the problem to someone else. Explaining the problem in order to another person, or to an inanimate listener, can also expose a faulty assumption, as covered in rubber duck debugging.
Common patterns worth checking
These are general categories, not a complete list:
- Off-by-one errors in loops and indices.
- Unexpected input, such as empty values, extremes or the wrong type.
- State that is modified in an unexpected place.
- Timing problems and concurrent access to shared data.
- Differences between environments, such as configuration, versions or permissions.
- Units and conversions that do not match, a classic source of numerical bugs addressed by dimensional analysis.
- Assumptions about hardware, such as timing, power or noise.
Fix and verify
When you believe you have found the cause, resist fixing the first symptom you see.
- Confirm that your explanation accounts for every observation, not only some of them.
- Make the smallest change that addresses the cause.
- Re-run the original failing case and confirm it now passes.
- Run the wider set of tests to check you have not broken something else. Good unit tests and integration testing make this far less stressful.
- For intermittent bugs, run enough repetitions to be confident, since a single pass proves little.
- Keep the reproduction as a regression test if practical.
Ask whether the same mistake could exist elsewhere. Searching for similar code or patterns often turns up relatives of the bug.
Look after yourself
Debugging can be draining. Fatigue produces careless mistakes, so take breaks, ask for help when you are stuck and avoid late-night changes to critical systems where you can. A fresh view from a colleague often notices what you have stopped seeing. Where a fault could affect safety or production, follow your organisation’s change and incident procedures, and review what happened afterwards in a blame-free way, as described under blameless reviews.
After the fix
Take a few minutes to consider what the bug teaches. Could a test, a check, a clearer interface or better documentation have prevented it? Capturing the lesson turns a frustrating hour into lasting improvement. The wider approach to tracing problems back to their origins is explored in root cause analysis.