← Back to Programming

Debugging and refactoring

Every programmer spends a lot of time finding and fixing bugs. A systematic approach to debugging, and safe techniques for improving existing code, save enormous time.

A debugging method

Effective debugging is scientific: reproduce the bug reliably; gather evidence such as error messages and logs; form a hypothesis about the cause; test it with an experiment; and repeat until confirmed. Then fix the cause, not just the symptom, and add a regression test. Guessing and changing random things wastes time and can introduce new bugs.

Reading error messages

Error messages and stack traces are clues. A Python traceback lists the chain of calls leading to the error, ending with the exact line and exception type, such as KeyError: 'email'. Read from the bottom up, find the first line in your own code, and check the values involved. Searching the exact message often finds explanations.

Debuggers and logging

A debugger lets you pause a program at breakpoints, step through lines, and inspect variables, as in VS Code, IntelliJ, or Python's pdb. Logging records events and values during normal runs, with levels such as DEBUG, INFO, WARNING, and ERROR, which is essential for problems in production where debuggers cannot be attached.

Narrowing the search

To locate a bug, shrink the problem: create a minimal example that still fails, comment out or bypass sections, or bisect. git bisect performs a binary search through commit history to find the change that introduced a bug, testing about log2(n) commits for n candidates. Explaining the code aloud, rubber-duck debugging, often reveals the mistake.

What refactoring is

Refactoring improves code's internal structure without changing its external behaviour. Martin Fowler's book Refactoring (1999) catalogued standard techniques. Refactoring is done in small steps, running tests after each one, so any mistake is caught immediately. Tests are therefore a precondition for safe refactoring.

Code smells

Code smells are signs that code may need refactoring: long functions, large classes, duplicated code, long parameter lists, deeply nested conditionals, and feature envy, where a method uses another class's data more than its own. A smell is a hint, not proof of a problem.

Common refactorings

Extract function moves a fragment into a well-named function. Rename clarifies meaning. Introduce parameter object groups related parameters. Replace conditional with polymorphism swaps type-checking if-chains for subclasses or strategies. Replace magic number with constant names a value. Modern IDEs perform many refactorings automatically and safely.

Worked example

A bug: an order total is sometimes 10% too high. Reproduce: it happens only for members. Hypothesis: the discount is applied as a surcharge. Experiment: log total before and after discount for a member order; after equals before x 1.1. The line reads total * (1 + rate) instead of total * (1 - rate). Fix it, add a test asserting a 100-dollar member order costs 90 dollars, and run the suite.

Key takeaways

Debug scientifically: reproduce, gather evidence, hypothesise, experiment, fix the cause, and add a regression test. Read stack traces from the bottom to find the failing line. Debuggers pause and inspect; logging records behaviour in production. Narrow bugs with minimal examples, bisection, and git bisect; rubber-duck debugging helps. Refactoring changes structure but not behaviour, in small tested steps. Code smells hint at refactoring needs. Extract function, rename, and replace conditional with polymorphism are common refactorings.

Flashcard

Reproducing a bug

Making it happen reliably

First debugging step.

Check your understanding

Why are automated tests important before refactoring?

← Previous lesson Next lesson →