Demo: a full bug hunt from report to fix
On This Lesson 11 topics
Lesson Overview
You now have every tool of the module: a method, breakpoints and stepping, variables and watches, stack traces, and a catalog of common errors. This lesson uses all of them on one case, from a vague bug report to a verified fix. It also shows a deliberate mistake, so you can see what random changes look like and how to recover from them.
The detective's picture is complete here. A report arrives. The detective visits the scene (reproduce), rules out suspects (isolate), forms a theory (hypothesize), checks it with evidence (test), closes the case (fix), and makes sure the right person was arrested (verify). Writing down the case notes at the end is part of the job.
Why this matters: this is the shape of the module's live debugging demonstration, and of the lab BEG-018-LAB1, where you will write a trail like the one below for five programs. In a real job the same trail goes into a ticket or a pull request description.
This lesson builds on the whole module and on edge-case thinking from BEG-016-L05. The debugger descriptions follow IntelliJ IDEA; menus and shortcuts vary by version.
Learning Objectives
By the end of this lesson you will be able to:
- Turn a vague bug report into a reproduction with input, expected, and actual values.
- Plan which breakpoint and which watches will test a hypothesis.
- Recognize a random change when you have made one, and undo it.
- Use the debugger to find a wrong condition, not just a wrong value.
- Verify a fix with the original case, a previously working case, and an edge case.
- Write a short case note that someone else could follow.
Written Explanation
The shape of a bug hunt
You will repeat the same sequence each time. Here it is as a checklist, with the question to ask at each step.
| Step | What you do | What you write down |
|---|---|---|
| 1. Read the report | Understand the claim, and find what is missing | Questions to ask |
| 2. Reproduce | Pick a small input; compute the expected result by hand | Input, expected, actual |
| 3. Isolate | Narrow to one method; compare a working and a failing input | The contrast |
| 4. Hypothesize | State a specific cause | One sentence |
| 5. Test | Use a breakpoint and watches to see whether the evidence agrees | The first surprising value |
| 6. Fix | Make one change that removes the cause | The change |
| 7. Verify | Re-run the original, an old working case, and an edge case | The three results |
The case note at the end is these rows in your own words: short enough to read quickly, clear enough that someone who was not there sees what was wrong, how you know, and why the change is right.
Reading a report critically
Reports are written by people who saw a symptom. Before you reproduce, ask:
- What input? "A student with a 0" is not specific. Which other grades?
- What did they expect, and what did they see? If the report does not say, work out the expected value yourself and confirm it with the reporter if you can.
- Is the rule clear? Does a grade of 0 count as a grade? In some systems a missing grade is stored as 0, and in others 0 is real. Check the rule before deciding that behavior is wrong.
In our case the report says a zero is a real grade, and so should count in the average.
Why the debugger beats guessing here
A wrong average can come from the sum, the count, the division, or the loop. The final result only says something is off. A breakpoint in the loop with two watches (sum and count) shows which one diverges from your trace, and when.
Recovering from a random change
Everyone tries something at random under pressure; the skill is to notice. Warning signs: you cannot say why you made the change; you ran only one input after it; or you did not recheck the other cases. Then undo the change, go back to the last evidence, and restart from the hypothesis step.
Examples
Example 1: the whole hunt, step by step (simple)
Report: "The grade average is wrong when a student has a 0."
The program:
static double average(int[] grades) {
int sum = 0;
int count = 0;
for (int grade : grades) {
if (grade > 0) {
sum += grade;
count++;
}
}
return (double) sum / count;
}Step 1: read. Zero counts as a grade (confirmed by the report).
Step 2: reproduce. Compute by hand first: for {80, 0, 100} the sum is 180, there are 3 grades, so the expected average is 60.0. Run:
{80, 90, 100} -> 90.0
{80, 0, 100} -> 90.0Expected for the second case 60.0, actual 90.0. A working case and a failing case: the failing one contains a 0.
Step 3: isolate. average is a single method; call it from a tiny main with just this array.
Step 4: hypothesize (first, weak). "Something in the loop ignores the 0." That is vague. Look at the clue: 90.0 equals 180 / 2. So the count was 2, not 3, while the sum was right (80 + 100 = 180 either way, since a zero adds nothing). A sharper hypothesis: the zero is not counted.
Step 5: test. Put a breakpoint on the if line. Add watches sum and count. Resume through the three passes, comparing with the hand trace:
| Pass | grade | expected sum, count after | debugger shows |
|---|---|---|---|
| 1 | 80 | 80, 1 | 80, 1 |
| 2 | 0 | 80, 2 | 80, 1 |
| 3 | 100 | 180, 3 | 180, 2 |
The first mismatch is on pass 2: count did not increase. Step over the if on that pass with grade = 0: the highlight skips the block. Evaluate grade > 0, which is false. The condition rejects the 0.
Step 6: fix. Change grade > 0 to grade >= 0.
Step 7: verify. All three: the original {80, 0, 100} now gives 60.0; the working case {80, 90, 100} still gives 90.0; the edge case {0, 0} gives 0.0. (Real run after the fix.)
Example 2: the planned mistake (practical)
Suppose that at step 4 we skipped the thinking and, under time pressure, tried "maybe the count is too small", changing the last line to:
return (double) sum / (count + 1);The real run printed:
{80, 90, 100} -> 67.5
{80, 0, 100} -> 60.0The failing case now shows 60.0, the expected value: it looks fixed. But the case that worked now prints 67.5, which is wrong (it should still be 90.0). Why did one case improve? By accident: with a zero in the data, the real count (3) happened to be one more than the loop's count (2). The change fits that case only, and silently breaks every other.
How to recognize this mistake:
- You could not say why you made the change. (Our reason was "maybe".)
- You tested only the failing input.
- The watches would have disagreed: with the new formula,
sum / (count + 1)no longer matches the hand trace for the working case.
Recovery: undo the change, return to the table of expected values, and go on from the hypothesis. This is the symptom fix from Lesson 1 made visible: it adjusts the answer for one input and leaves the cause (the condition) untouched.
Example 3: write the case note (realistic)
A short case note for a ticket or a pull request:
Bug: Class average ignored students with a grade of 0.
Reproduce: grades {80, 0, 100}. Expected 60.0, actual 90.0.
Isolated to: average(int[]), loop condition.
Hypothesis: "if (grade > 0)" skips zero, so count is 2 instead of 3.
Evidence: breakpoint in loop with watches sum and count; on the pass
with grade = 0, count did not increase (condition false).
Fix: changed "grade > 0" to "grade >= 0".
Verified: {80,0,100} -> 60.0; {80,90,100} -> 90.0; {0,0} -> 0.0.
Open question: an empty array gives NaN (0 / 0 as double); decide the
desired result separately.The last line shows a good habit: you found an adjacent problem (an empty array printed NaN in our run) and recorded it instead of quietly changing more. One fix per change keeps the evidence clean.
Backend Insight
Teams record this trail: a ticket with reproduction steps, the fix in a pull request, and ideally an automated test that fails before the fix and passes after. The case note above holds the inputs such a test needs. If the bug returns months later, that record saves hours.
It also explains the rule "one fix per change": mixed changes are hard to review and hard to undo.
Common Mistakes
Mistake 1: Starting from the code instead of the report
Reproduce first, with a hand-computed expected value. Otherwise you do not know what "fixed" means.
Mistake 2: Staying with a vague hypothesis
"Something is wrong in the loop" is not testable. Use the clue: 90.0 equals 180 / 2 pointed straight at the count.
Mistake 3: Verifying only the failing case
The random change in Example 2 passed the failing case and broke the working one.
Mistake 4: Fixing several things at once
If you also change the empty-array behavior in the same edit, you cannot tell which change helped, and a review cannot either.
Mistake 5: Not writing it down
You will not remember tomorrow why >= is correct. The note is for you, too.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. In the grade bug, which two values would you watch inside the loop, and why?
Show solution A1
A1. sum and count. The average depends on both, and comparing each to the hand trace after each pass shows which diverged and when.
A2. Why is it important to compute the expected value by hand before running?
Show solution A2
A2. Without an expected value you cannot tell whether the actual output is wrong, and a believable wrong answer can fool you. The hand value is also what you verify the fix against.
A3. Name two warning signs that you have made a random change.
Show solution A3
A3. You cannot explain why you made the change; you tested only the failing input afterwards; one input improved while others were not rechecked; the change adds a constant ("+ 1") with no reason.
Level B: Application
B1. This method should count marks that pass, where 50 or more is a pass:
static int countPassing(int[] marks) {
int passing = 0;
for (int m : marks) {
if (m > 50) {
passing++;
}
}
return passing;
}For {40, 50, 70} it returns 1. Write a case note with all seven rows, including an edge-case verification.
Show solution B1
B1. Report: counts are wrong at the pass mark. Reproduce: {40, 50, 70}, expected 2 (50 and 70 pass), actual 1. Isolate: the loop and condition in countPassing; compare with {40, 60, 70}, which returns 2 correctly, so the difference is a mark of exactly 50. Hypothesize: m > 50 excludes 50, and the rule is 50 or more. Test: breakpoint on the if, watch m > 50; on the pass with m = 50 it is false, and passing does not increase. Fix: m >= 50. Verify: {40, 50, 70} gives 2; {40, 60, 70} still gives 2; edge cases {49} gives 0 and {50} gives 1. (A real run of the buggy version printed 1.)
B2. A teammate changes the grade method to return (double) sum / (count + 1); and says "it fixed the report". Explain with two inputs why that change is wrong.
Show solution B2
B2. For {80, 90, 100} the correct average is 90.0, but dividing by count + 1 gives 270 / 4 = 67.5 (a real run printed 67.5). For {80, 0, 100} it happens to give the right 60.0 only because the buggy loop skipped the zero. The change compensates for one input and breaks the others, and the real cause is the condition.
B3. Which line of a case note makes it useful months later, and why?
Show solution B3
B3. The reproduction (input, expected, actual): anyone can re-run it, and it can become an automated test. The evidence line also shows the fix is based on observation.
Level C: Challenge
C1. This method should reverse an array in place, but {1, 2, 3, 4, 5} comes out unchanged:
static void reverse(int[] a) {
for (int i = 0; i < a.length; i++) {
int j = a.length - 1 - i;
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}Write the hand-trace of the first four passes, state the hypothesis, say what you would watch, and give the fix and three verification inputs.
Show solution C1
C1. Pass 1: i = 0, j = 4, swap 1 and 5, giving 5 2 3 4 1. Pass 2: i = 1, j = 3, swap 2 and 4: 5 4 3 2 1. At this point the array is reversed. Pass 3: i = 2, j = 2, nothing changes. Pass 4: i = 3, j = 1, swaps 2 and 4 back: 5 2 3 4 1. Pass 5 swaps the ends back: 1 2 3 4 5. Hypothesis: the loop runs the full length, so each pair is swapped twice and the array ends where it began. Watch i, j, and the array; the array is correct after pass 2 and wrong again later. Fix: loop i < a.length / 2. Verify (real run of the fixed version): {1, 2, 3, 4, 5} gives [5, 4, 3, 2, 1], {1, 2, 3, 4} gives [4, 3, 2, 1], and {9} gives [9]. The buggy version printed [1, 2, 3, 4, 5].
C2. Write the case notes for a bug of your own that you fixed in an earlier module, in the format of Example 3.
Show solution C2
C2. Any bug that you can describe with the rows: input, expected, actual, isolation, hypothesis, evidence, fix, verification. Check that the hypothesis is specific and that verification includes more than the failing case.
Mini Challenge
Prepare for the lab with a dry run. Copy the Grades program from Example 1 into your IDE with the bug in place, and perform the whole hunt yourself without looking at the answer: write the reproduction first, then debug with a breakpoint and watches. Then add a second bug of your own, such as count++ outside the if, and do the hunt again, writing the case note.
Check your work: each note has a hand-computed expected value, a first mismatch found with the debugger, one change, and at least three verification inputs.
Summary & Next Lesson
Key takeaways
- A bug hunt is the six steps with a report on one end and a case note on the other.
- Compute the expected value by hand, then use breakpoints and watches to find the first mismatch.
- A random change can fix one case and break another: check an old working case too.
- Fix the cause with one change, and record adjacent problems separately.
- Write the case note: input, expected, actual, hypothesis, evidence, fix, verification.
Next lesson
This is the last lesson of the module. The next step is the lab BEG-018-LAB1, "Bug hunt": five programs, each with one logic bug, and a written trail for each. Use the case note in Example 3 as your template.
Self-check
- What did the clue
90.0 = 180 / 2tell you in the grade bug? - Why did
(count + 1)seem to work? - Why verify with a case that worked before?
- What goes into a case note?
- Why record the empty-array problem instead of fixing it in the same change?
Show answers
- That the sum was right but the count was 2 instead of 3, so the zero was not counted.
- By accident: it matched the failing input only, because the loop had skipped exactly one grade.
- To catch a change that fixes one case and breaks another.
- Input, expected, actual, where it was isolated, hypothesis, evidence, fix, and verified cases.
- One change at a time keeps the evidence clear and the change reviewable.
References
- JetBrains, Examine a suspended program. Supports the use of the Variables tab, watches, and Evaluate Expression in the test step.
- JetBrains, Breakpoints. Supports line breakpoints in the gutter.
- Oracle, ArithmeticException. Supports the contrast with integer division. The
NaNresult of(double) 0 / 0was confirmed only by running the code with JDK 21. - All program outputs (
90.0,60.0,67.5,0.0,NaN, the reversed arrays, and the pass-count of 1) come from compiling and running the code with JDK 21. The process table and case-note format are this course's own teaching framework. The debugger tables show what the described watches would display and were derived from the code, not recorded from an IDE session.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.