How to Debug Code: A Step-by-Step Method and Tools
What is debugging?
Debugging is the process of finding the cause of a defect in a program and fixing it. A good debugging process is systematic: reproduce the problem, read the error, narrow down where it happens, form a hypothesis, test it, apply a fix and add a test so the bug cannot quietly return. Tools such as debuggers and logs make each step faster.
Bugs, errors and stack traces: the key terms
- Bug: any behaviour that differs from what the program should do.
- Syntax error: code the language cannot parse, reported before the program runs.
- Runtime error (exception): a failure while the program runs, such as dividing by zero or reading a missing key.
- Logic error: the program runs without crashing but gives the wrong result. These are often the hardest to find.
- Stack trace (traceback): the list of function calls that were active when an exception happened, ending with the line that failed.
- Breakpoint: a marker that pauses the program at a line so you can inspect variables.
The debugging process, step by step
- Reproduce the bug reliably. Write down the exact input, steps and environment. A bug you cannot reproduce is a bug you cannot confirm as fixed.
- Read the error message and stack trace in full. Note the exception type, the message and the last line of your own code in the trace.
- Isolate the problem. Remove unrelated code or data until you have the smallest input that still fails.
- Hypothesise a specific cause, such as "the loop stops one item early".
- Test the hypothesis by printing a value, pausing at a breakpoint or changing one thing at a time.
- Fix the root cause, not the symptom, and run the original reproduction again.
- Add a test that fails without the fix and passes with it, so the bug cannot return unnoticed.
Reading a Python traceback
Python prints the traceback with the most recent call last, so read it from the bottom up. The final line gives the exception type and message; the lines above show the path the program took to get there.
Traceback (most recent call last):
File "report.py", line 9, in <module>
print(average([]))
^^^^^^^^^^^
File "report.py", line 5, in average
return total / len(marks)
~~~~~~^~~~~~~~~~~~
ZeroDivisionError: division by zeroHere the failing line is line 5 inside average, and the cause is an empty list reaching it from line 9. The fix is a decision about behaviour: should an empty list return 0, return None or raise a clearer error? Answer that question, then change the code.
Worked example: fixing an off-by-one bug
A function should return the last n daily sales figures, but users report that the latest day is always missing.
def last_n_days(sales: list[int], n: int) -> list[int]:
result = []
for i in range(len(sales) - n, len(sales) - 1): # bug
result.append(sales[i])
return result
print(last_n_days([120, 90, 150, 200, 175], 3)) # [150, 200], expected [150, 200, 175]Reproduce: the input above shows the problem every time. Isolate: the loop is the only place items are chosen. Hypothesise: range excludes its stop value, so stopping at len(sales) - 1 skips the final index. Test: printing i inside the loop shows 2 and 3, never 4. Fix and add a test:
def last_n_days(sales: list[int], n: int) -> list[int]:
if n <= 0:
return []
return sales[-n:]
def test_last_n_days() -> None:
assert last_n_days([120, 90, 150, 200, 175], 3) == [150, 200, 175]
assert last_n_days([120, 90], 5) == [120, 90]
assert last_n_days([120, 90], 0) == []
test_last_n_days()
print("all tests passed")The slice is simpler than the loop, and the tests cover the edge cases the original missed: asking for more days than exist, and asking for zero days. Off-by-one errors are common wherever loops and indexes meet, which is why the searching and sorting algorithms guide pays close attention to loop bounds.
Debugging tools and when to use them
| Tool | What it does | Use it when |
|---|---|---|
| Print and log statements | Show values at chosen points as the program runs | You need a quick look, or the bug appears only in production logs |
| Browser DevTools | Console, network panel and the Sources panel for JavaScript breakpoints | Front-end code misbehaves or a request fails |
Python breakpoint() and pdb | Pauses execution and opens an interactive debugger | You need to step through Python code line by line |
| Xdebug for PHP | Step debugging in an editor such as VS Code or PhpStorm | You need to inspect PHP variables mid-request |
git bisect | Binary search through commit history to find the change that introduced a bug | Something worked in an older version and is broken now |
| Rubber duck debugging | Explaining the code line by line, out loud, to an object or person | You are stuck and need to question your assumptions |
Using pdb in Python
Add breakpoint() on the line before the suspected problem and run the script. At the (Pdb) prompt, p name prints a variable, n runs the next line, s steps into a function call, c continues and q quits. Our introduction to Python covers the language features you will be inspecting.
Breakpoints in browser DevTools
Open DevTools (F12), go to the Sources panel, open your script and click a line number to set a breakpoint. Reload or trigger the action; execution pauses there and you can hover over variables, watch expressions and step through code. This is often quicker than adding console.log calls everywhere, especially in DOM manipulation code.
Finding the commit with git bisect
git bisect start
git bisect bad # the current commit is broken
git bisect good a1b2c3d # this older commit worked
# Git checks out a commit halfway between; test it, then run
# git bisect good or git bisect bad until Git names the culprit
git bisect reset Habits that prevent bugs and speed up debugging
- Write small functions that do one thing, so a failing function has fewer places for the bug to hide.
- Use logging with levels (debug, info, warning, error) instead of leaving print statements in finished code.
- Never show detailed error messages to visitors on a live site; log them on the server instead, as covered in secure PHP practices.
- Commit often with clear messages, so tools such as bisect have small steps to search.
- Keep automated tests for every bug you fix; over time they form a safety net.
Practical exercise: debug and protect a buggy function
- Write a function
count_passes(marks, pass_mark)that counts marks greater than or equal to the pass mark, but deliberately use>instead of>=. - Call it with
[35, 40, 72, 40, 18]and a pass mark of 40, and note the wrong answer. - Add
breakpoint()inside the loop and usep markandnto watch which marks are counted. - Fix the comparison and write a test function with at least three assertions, including an empty list.
You are done when the function returns 3 for the example, your tests pass, and you can explain in one sentence what the bug was and how the debugger showed it. The PHP Foundation course practises the same debugging habits on PHP projects.
Common mistakes
- Changing code at random until the error disappears, without understanding the cause.
- Reading only the first line of a stack trace instead of finding the last line of your own code in it.
- Fixing the symptom, such as wrapping everything in a try block, while the real cause stays in place.
- Forgetting to remove debugging print statements or
breakpoint()calls before committing. - Not writing a test for the bug, so the same mistake returns in a later change.
Frequently asked questions
Is print debugging bad practice?
No. Printing values is a quick, valid way to check a hypothesis. For longer investigations a debugger is usually faster, and for production code structured logging replaces temporary prints.
How do I debug a bug I cannot reproduce?
Gather more information first: add logging around the suspected area, record inputs and environment details, and compare cases that fail with cases that work. Once you find a pattern, you can usually build a reliable reproduction.
What is Xdebug used for?
Xdebug is a PHP extension that adds step debugging and improved error output. Connected to an editor, it lets you pause a PHP script at a breakpoint and inspect variables, which helps when working on server-side PHP code.