The debugging topics of this textbook draw substantially on The Debugging Book by Andreas Zeller et al., in particular its terminology of defects, infections, and failures, and its treatment of debugging as an application of the scientific method. Further material was adapted from the debugging readings of MIT 6.102, UC Berkeley CS61B, Stanford CS107, UW CSE 332, and CMU 15-213.
Debugging is the process of finding the cause of a known problem in a program, and fixing it. The hard part is usually the diagnosis rather than the correction.
To debug well, we distinguish four things that we loosely refer to as 'the bug':
1.These four form a chain, and each link can be far from the next:
A big part of debugging is the backward search for the defect by starting from the observed failure.
This chain also explains why bugs hide: a defect infects the state only when that line executes, and an infection becomes a failure only if it propagates out to something observable. So, a defect can sit in daily-executed code for months unnoticed.
Example The shopping cart code given below prints the correct total, but then the cart appears empty.
class Cart {
private final List<Item> items = new ArrayList<>();
void add(Item item) {
items.add(item);
}
List<Item> getItems() {
return items;
}
int computeTotal() {
int total = 0;
List<Item> pending = getItems();
while (!pending.isEmpty()) {
total += pending.remove(0).price();
}
return total;
}
}
Cart's intended contracts: computing a total must not change the cart, and getItems() lets callers read the items without owning the list.
getItems() hands back a copy.getItems() hands out the live internal list, which computeTotal() then empties — breaking both contracts at once.computeTotal() returns, the cart's own items list is empty.Some factors that make debugging hard are:
catch block.What these have in common is that they produce activity without producing useful information. A productive debugging step is one that rules something out.
Adding temporary print statements is not necessarily bad. A few prints chosen to answer a specific question are legitimate, and in production, embedded, or concurrent settings they are sometimes the only tool available. However, this technique alone is not enough to tackle most non-trivial bugs.
Systematic debugging involves following tasks, whose first letters make up the mnemonic TRAFFIC (credit: Andreas Zeller).
TRAFFIC is a map rather than a mandatory sequence. Some tasks can be done in a loop before it lands on the cause, and you can reorder the other tasks as you see fit.
The most common mistake is jumping straight to C. Starting at 'correct' and working backwards is how shotgun debugging happens. This text below covers the six tasks TRAFFIC except C, which is covered separately.
Know when to stop for the day: Debugging is unusually sensitive to fatigue, because the whole activity consists of holding a model of the program in your head.
The first step of debugging should be stating what the correct behavior is, and why. Without that you have nothing to compare the program against, and you risk searching code that was right all along. State the expectation in a testable form: for this input, that exact result.
Sometimes the fault is in the test, not in the code under investigation. It is worth considering early, because it is easy to lose hours to a test that was wrong all along.
A reliable reproduction makes every experiment cheap and is a way to confirm afterwards that the fix worked. Reproducing means recreating everything the failure depends on, e.g., the input data, the program version, the environment and , the sequence of actions, and the starting state, such as leftovers from a previous run.
When you cannot reproduce a failure you can still investigate it. Instead of running experiments you mine the evidence left behind: stack traces, logs, , , and the differences between runs that failed and runs that did not. The immediate goal becomes making the failure more observable or more frequent. Moving a bug from 'once a week' to 'one run in five' is real progress.
Automate the reproduction as a test case as early as you can. Turning "launch the app and perform these six steps" into a one-second command is what makes the hypothesis loop cheap, and it can become the regression test once you have a fix.
Why simplify? The smaller the failing case, the smaller the search space -- every element you can remove while the failure persists eliminates a whole category of possible causes. Some directions you can try:
Example A 500-line configuration file makes the app crash at startup. Halving gets nowhere: neither half crashes, because the failure needs one setting from each. Removing settings one at a time from the full file isolates the pair — a theme entry and a locale entry, each harmless alone.
An origin is a place where the state could first have gone wrong: before it the state is correct, after it the state is infected, and the cause sits at that boundary. This step often produces a list of candidate origins rather than a single answer. Some techniques you can follow:
Candidate origins are not equally likely, and the order you check them in decides how long the search takes.
Isolating means running one check, discarding the part of the search space it rules out, and repeating until you find what needs to be fixed.
git bisect automates this, and works best with small, self-contained commits. Martin Fowler calls this Diff Debugging.Example Binary search along the execution, on a run too long to watch: a 10,000-row import produces the right running total at the start and the wrong one at the end, and nothing in between is visible. Pause at row 5,000 and ask one question — is the total already wrong? If it is, the cause lies in the first half, so pause next at row 2,500; if it is not, pause at row 7,500. Fourteen such checks reduce 10,000 rows to one, and none of them requires understanding the code — only the ability to say whether the state is already wrong.
The cheapest bug to debug is the one that announces itself, and most of what makes code debuggable is decided long before the bug exists.
@Override, generics, final. A defect caught here costs no debugging at all.Each of these shortens the distance between defect and failure, which is the root of the difficulty.
Covered in a separate section, titled Fixing.
Do not settle on a fix until you can explain the whole failure. Temporary changes made as experiments are fine, but a change you intend to keep needs a causal account that explains every observed behavior. The strongest check is to predict, before making the change, exactly what will be different afterwards, then verify that prediction. A fix that works for reasons you cannot state can fail again.
Fix the cause, not the infection and not the failure. Special-casing the failing input or clamping a bad value removes the symptom and leaves the cause. Also consider whether you have a coding error or a design error: a coding error is code that does not do what you intended -- whereas a design error means the intention itself was wrong. The real remedy for the latter is a design change, not a code fix.
Which change counts as 'the fix' is sometimes a genuine choice. Making that choice consciously helps ensure that the fix does not degrade other aspects of the system (e.g., its design integrity).
Example In the cart example you could make getItems() return a copy, or make computeTotal() iterate without mutating. Both remove the failure; they differ in which contract you treat as authoritative.
Once you have a candidate fix, finish the job:
Example The cart example, end to end:
computeTotal() empties the cart every time.computeTotal(), then asserts on getItems().size(). One item is enough to fail, so the test uses one.items is the loop in computeTotal(), and size() dropping from 3 to 0 across the call — with pending and items confirmed to be the same object — settles it.getItems() returning a copy and computeTotal() iterating without mutating would remove the failure. Choose one and implement it.Every way of looking inside a running program is a probe — a means of answering one specific question about its state. Here are some widely used probes:
assert statement, enable assertions in your run configuration or it will do nothing; test-framework (e.g., JUnit) assertions are separate and always run during testing.The useful question is never "print statements or debugger or ___?" but "what is the cheapest probe that answers this question?" Some probes come out once the bug is found (e.g., a temporary print statement); others are meant to stay (e.g., a permanent log statement).
As a rough guide:
A debugger is a tool that lets you pause a running program, then inspect and control it from the inside, without modifying its code. You choose the lines where the program should stop. When execution reaches one of them, the program freezes mid-run and waits for you. You can then look at the current values of its variables, see the chain of method calls that led there, and let it continue one line at a time. Most IDEs, such as IntelliJ IDEA, come with a debugger built in. There are also standalone debuggers that you control by typing commands, such as GDB for C and C++, and jdb for Java.
If you have not used a debugger tool before, watching a 'debugger in action' video such as the one below is recommended before reading the rest of this topic.
A debugger works from outside your program, which is what makes it different in kind from printing. The debugger runs as a separate process. It either launches your program under its control or attaches to one that is already running, then asks the operating system, or the JVM for a Java program, to pause the program at the chosen lines and report its state back. Your code stays untouched throughout. With print statements, every new question about the program means adding another print, rebuilding, and re-running. With a debugger, the paused program keeps all its state, so asking one more question costs seconds rather than another edit-build-run cycle.
Breakpoints determine where the program pauses. A breakpoint is a marker that tells the debugger to stop the program at a particular spot. In an IDE, you usually set one by clicking in the margin next to a line number, which puts a red dot there. In GDB, you type a command such as break main.c:42 instead. Either way, the debugger keeps track of the breakpoint, so your source file does not change. Breakpoints take effect only when you start the program through the debugger, e.g., with IntelliJ's Debug button rather than Run. Started the normal way, the program runs straight past them. Breakpoints come in several kinds:
i == 4137.null?"Stepping commands determine how execution advances. Once you have examined the paused program, a stepping command lets it run a little further and then pauses it again, so you can see what changed. In an IDE, each command has a toolbar button and a keyboard shortcut. The common ones are:
total += item.price();, step over pauses at the line after it, while step into pauses inside price().Inspection views let you examine details of the state the program is currently paused at. Some common inspection views:
Example One session on the cart example, from the first breakpoint to the diagnosis:
total += pending.remove(0).price();, then run the code that adds three items and calls computeTotal().pending holding all three items, and nothing yet looks wrong.pending == items answers true. That single evaluation is the diagnosis — the list being emptied is the cart's own — and it cost no edit, no rebuild, and no re-run.computeTotal() called from the display code, so the frame the failure will surface in is not the frame the defect is in.items.size() fall in the variables view as the loop runs.A field watchpoint on items would not have helped here: items is assigned once, where it is declared, so the watchpoint fires at construction and never for remove(0). A watchpoint catches a field being reassigned, not the object it already points at being modified.
Set your first breakpoint before the suspected region rather than at the failure, so you can watch the state go wrong.
AI assistants are useful for some parts of debugging and unreliable for others. They are good at explaining unfamiliar error messages, proposing candidate hypotheses, and serving as an always-available rubber duck. They are unreliable at diagnosing a defect in code they cannot run, and will produce confident, fluent, incorrect explanations. A systematic method is what makes them safe: treat any suggestion as a hypothesis, insist it be falsifiable, and verify it against the running program yourself.
A stack trace is a precise report of where a program failed and the call path that led there. Read it in this order:
The top of the trace is where the failure surfaced, but the cause is often further down, in whichever frame passed the bad value along. In wrapped exceptions, read the Caused by: chain from the bottom up.
Some common Java exceptions and what they usually mean:
| Exception | Usually means |
|---|---|
NullPointerException | Something never initialized, or a method returning null unnoticed |
IndexOutOfBoundsException | An off-by-one, or an index computed from stale size information |
ClassCastException | An object that is not the type assumed, often after an unchecked cast |
ConcurrentModificationException | A collection modified while being iterated over, usually in a single thread Example removing from a list inside a for-each loop over that list |
StackOverflowError | Recursion with a missing or unreachable base case |
NumberFormatException | Unvalidated input being parsed as a number |