Debugging Fundamentals
Debugging is the process of finding why a program does not behave as expected and fixing the cause. It works best as a series of small experiments: form a hypothesis, test it, and learn from the result.
A method that works
- Reproduce the problem reliably. See Reproducing Bugs.
- Read the evidence. Read the whole error message and the stack trace (see Error Handling), starting at the first line that points into your own code.
- Form a hypothesis. State what you think is wrong, and what you would see if you were right.
- Test it with the smallest experiment you can find: a log line, a breakpoint, a modified input.
- Fix the cause of the problem, then confirm the original scenario works.
- Add a test that fails without the fix, so the bug cannot come back unnoticed. See Unit Testing.
Change one thing at a time. If you change three things and it works, you do not know which one mattered.
Using a debugger
A debugger runs your program under your control. Learn these features. A breakpoint pauses execution at a chosen line. A conditional breakpoint pauses only when an expression is true, which is useful inside loops. Stepping comes in three kinds: step over (run the line), step into (enter the called function) and step out (finish the current function). The call stack shows how execution reached this point. Variables and watch expressions show the actual values, which are often not what you assumed.
Most editors and IDEs, including VS Code and Visual Studio, offer these features for most languages.
When a debugger is not enough
- Print or log statements work everywhere, including in production and in concurrent code where pausing changes the behaviour. See Logging Fundamentals.
- Halve the search. Comment out half of the suspect code or inputs and see which half still fails. With version control,
git bisectdoes the same across commits: mark a known good and a known bad commit, and Git guides you to the first bad one. - Explain the problem aloud. Describing it to a colleague, or to a rubber duck, often exposes the faulty assumption.
Habits that waste time
- Guessing and changing code at random.
- Assuming the bug is in the library or compiler before checking your own code.
- Leaving debugging code behind. Remove temporary logging and breakpoints before committing.
- Stopping at the first fix that “works” without understanding why. For deeper analysis see Root Cause Analysis and Troubleshooting Methodology.
Try it
Write a small function with a deliberate bug, for example one that returns the wrong result for an empty list. Run it under your debugger, set a breakpoint, step through it, and inspect the variables until you can state the cause in one sentence. Then write a test that fails before your fix and passes after.
Common mistakes
- Guessing and changing code at random.
- Blaming the library or compiler before checking your own code.
- Leaving debugging code behind when committing.
- Stopping at a fix that works without understanding why.
Check yourself
- What is the difference between step over, step into and step out?
- Why change only one thing at a time?
- How does git bisect find the commit that introduced a bug?