Online admissions open for the SEO, PHP web development and networking & cybersecurity courses. Enquire for the next batch.

How to Debug Code: A Step-by-Step Method and Tools

6 min read Updated By the SkillNewton team

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

  1. 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.
  2. 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.
  3. Isolate the problem. Remove unrelated code or data until you have the smallest input that still fails.
  4. Hypothesise a specific cause, such as "the loop stops one item early".
  5. Test the hypothesis by printing a value, pausing at a breakpoint or changing one thing at a time.
  6. Fix the root cause, not the symptom, and run the original reproduction again.
  7. 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 zero

Here 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

ToolWhat it doesUse it when
Print and log statementsShow values at chosen points as the program runsYou need a quick look, or the bug appears only in production logs
Browser DevToolsConsole, network panel and the Sources panel for JavaScript breakpointsFront-end code misbehaves or a request fails
Python breakpoint() and pdbPauses execution and opens an interactive debuggerYou need to step through Python code line by line
Xdebug for PHPStep debugging in an editor such as VS Code or PhpStormYou need to inspect PHP variables mid-request
git bisectBinary search through commit history to find the change that introduced a bugSomething worked in an older version and is broken now
Rubber duck debuggingExplaining the code line by line, out loud, to an object or personYou 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

  1. Write a function count_passes(marks, pass_mark) that counts marks greater than or equal to the pass mark, but deliberately use > instead of >=.
  2. Call it with [35, 40, 72, 40, 18] and a pass mark of 40, and note the wrong answer.
  3. Add breakpoint() inside the loop and use p mark and n to watch which marks are counted.
  4. 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.

Want to Learn This Properly?

The PHP Web Development: Foundation & Practical Implementation (Online) turns guides like this into practised skills, with projects on real websites.

WhatsApp Call Us Now

Free counselling

Send an Enquiry

Tell us which course you are interested in. We reply by email or phone with the next batch and how to enrol.

We reply by email or phone within working hours. No spam.