Debugging: Compile-Time, Run-Time, and Logic Errors

The last lesson: everything the book has said about debugging, organised by the question that decides the strategy — is this a compile-time, run-time or logic error? Includes bisection, tracing a loop's condition, reading a stack trace, the minimal test case, and the rubber duck. Follows Think Java 2e, Chapter D (Debugging), Sections D.1-D.3, pp. 327-340, cross-referenced against The Java Tutorials — What Is an Exception?.

Subject: Java · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Debugging: Compile-Time, Run-Time, and Logic Errors

Title

Think Java 2e · Chapter D · Debugging

Sections D.1-D.3 · pp. 327-340

2. What you will be able to do

Objectives

This lesson follows Think Java 2e, Chapter D (Debugging), Sections D.1-D.3, pp. 327-340. Everything on these slides can be checked against those pages.

1. Classify an error as compile-time, run-time or logic, and choose a strategy accordingly.

2. Explain why only the first compiler error message is reliable.

3. Use debugging by bisection when a program will not compile at all.

4. Diagnose a hanging program by distinguishing an infinite loop from infinite recursion.

5. Read a stack trace and name the likely cause of the common exceptions.

6. Find a minimal test case, and break a complex expression into temporary variables.

3. Retrieve before you read

Warm-up

Everything in this appendix has appeared somewhere already.

Discussion prompt

From Lesson 2b: what are the three kinds of error? From Lesson 8a: what does a StackOverflowError mean? And from Lesson 11b: why should you not compare doubles with ==?

Hint: Compile-time, run-time, logic. And missing base case.

Answer:

The three kinds are compile-time, run-time and logic errors. A StackOverflowError usually means an infinite recursion — a missing or unreachable base case. And doubles are approximate, so == fails on values that ought to be equal.

Although there are debugging suggestions throughout the book, we thought it would be useful to say more in an appendix. If you are having a hard time debugging, you might want to review this appendix from time to time. It is a reference, not a narrative — and this deck is the map of it.

4. Which kind of error is it?

Concept

The best debugging strategy depends on what kind of error you have. That one question splits the whole appendix into three, and answering it first saves the most time.

Figure (svg): Three boxes showing compile-time, run-time and logic errors with an example of each

Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter D (Debugging), Sections D.1-D.3, pp. 327-340 — Appendix D begins on printed page 327.

5. Compile-time errors

Section

Section D.1

6. Only the first message is reliable

Concept

If the compiler reports 100 error messages, that doesn't mean there are 100 errors in your program. When the compiler encounters an error, it often gets thrown off track for a while. It tries to recover and pick up again after the first error, but sometimes it reports spurious errors.

Foo.java:12: error: ';' expected
Foo.java:14: error: illegal start of expression
Foo.java:14: error: ';' expected
Foo.java:19: error: class, interface, or enum expected
... 96 more
messagetrustworthy?
the firstyes
the restpossibly spurious
the countnot the number of errors

Only the first error message is truly reliable. We suggest that you fix only one error at a time and then recompile the program. You may find that one semicolon or brace fixes 100 errors.

7. Where the error actually is

Notation

The line number is a starting point, and the appendix is precise about what it really means.

Annotate

  • Where it noticed, not where it is. A missing semicolon on line 12 is often noticed on line 13 or 14.
  • So look earlier, not later — the compiler reads forward and only realises something is wrong when the next thing does not fit.
  • For example, if you get an error message at a method invocation, the actual error may be in the method definition itself.
  • First of all, read the error message carefully. It may be written in terse jargon, but often there is a carefully hidden kernel of information.
  • Make sure the program is indented properly; that makes it easier to spot syntax errors — a mismatched brace is obvious in indented code and invisible otherwise.

Read the message, look before the line it names, and fix one thing at a time. Three habits, and together they turn a hundred messages into one edit.

8. The nine things to check

Worked example

Now, start looking for common syntax errors. The list is worth reading through once, slowly.

1. parentheses and brackets balanced and properly nested
2. uppercase letters are not the same as lowercase
3. semicolons at the end of statements (and NOT after braces)
4. matching quotation marks - double for strings, single for chars
5. assignment: the type on the left matches the type on the right
6. method calls: arguments in the right order, with the right types
7. value methods: use the result. void methods: do not
8. instance methods: on an object. static methods: name the class
9. instance variables cannot be used from a static context
itemthe lesson that covered it
braces and nesting5a — always write the braces
case sensitivity1b
semicolons1a
single vs double quotes6b — char against String
types on both sides of =2a
argument order4a — two ints are easy to swap
void methods return nothing4b
static context13b — the two error messages

Check the structure first.

Why: All method definitions should be nested within a class definition. All program statements should be within a method definition.

Then the punctuation.

Why: Semicolons, quotes, and — item 3's parenthesis — no semicolons after curly braces.

Then the types.

Why: Items 5, 6 and 7 are all does this expression have the type this position needs.

Then the static rules.

Why: **If you try that in a static method — with or without this — you get a message like non-static variable x cannot be referenced from a static context.**

Verify: Notice that every item on the list appeared in an earlier lesson.

Why: This appendix is a summary, not new material. Its value is being a checklist you can work through when you are frustrated — which is exactly when you stop thinking systematically.

9. A hundred error messages

Prediction

The compiler reports 100 errors.

Foo.java:12: error: ';' expected
... 99 more messages
messagesactual errors
100?

Predict first

How many errors are probably in the program?

  • Possibly one — a single semicolon or brace can produce a hundred messages
  • Exactly 100
  • At least 50
  • There is no way to tell, so fix them all at once

Correct: Possibly one — a single semicolon or brace can produce a hundred messages

Why: When the compiler encounters an error, it often gets thrown off track for a while. Only the first error message is truly reliable. You may find that one semicolon or brace fixes 100 errors. Fix one, recompile, and look at the new first message.

10. The best debugging is none

Concept

The best kind of debugging is the kind you don't have to do because you avoid making errors in the first place.

techniquefromwhat it prevents
incremental developmentLesson 4bnot knowing where the error is
always writing bracesLesson 5aa whole class of scope bugs
named constantsLesson 3amagic numbers in two places
automated testsLesson Aa change breaking something silently

Incremental development can help. The key is to start with a working program and add small amounts of code at a time. When there is an error, you will have a pretty good idea of where it is. That is the whole strategy in one sentence: make the search space small before you need to search it.

11. Doing what the compiler tells you

Trap

The trap

Some error messages come with unreliable advice.

error: class Golfer must be declared abstract. It does not
define int compareTo(java.lang.Object) from interface
java.lang.Comparable.

// so you declare Golfer abstract... and now nothing works
the compiler suggestswhat is actually wrong
declare Golfer abstractGolfer is missing a compareTo method
does the suggestion compile?yes
does it fix the problem?no

It sounds like the compiler is telling you to declare Golfer as an abstract class, and if you are reading this book, you probably don't know what that is or how to do it. Fortunately, the compiler is wrong.

The fix

Treat the message as evidence, not instruction.

// The solution in this case is to make sure Golfer has a
// method called compareTo that takes an Object as a parameter.

public int compareTo(Object o) { ... }
what a message gives youreliable?
that something is wrongyes
roughly where it was noticedyes
what the problem isusually
what to do about itnot reliably

Don't let the compiler lead you by the nose. Error messages give you evidence that something is wrong, but the remedies they suggest are unreliable. A compiler knows what does not fit; it does not know what you meant.

12. Which kind of error?

Definition probe

The strategy depends on the answer.

Sort into buckets

Sort each symptom.

compile-time
a missing semicolon; cannot find symbol
run-time
a StackOverflowError while running
logic
the answer is off by one
ct
Something is wrong with the syntax of the program, and the compiler refuses to produce byte code at all.
rt
Something goes wrong while the program is running — it compiled fine and then failed.
lg
The program runs to completion and does the wrong thing. Nothing reports it, because only you know what it was supposed to do.

13. Where is the error?

Prediction

The message names line 14.

11: int x = 5
12: int y = 3;
13:
14: System.out.println(x + y);
    // error reported here
detail
line named14
line 11no semicolon

Predict first

Where should you look first?

  • Before line 14 — the compiler reports where it noticed the problem, not where it is
  • Exactly on line 14
  • After line 14
  • In a different file

Correct: Before line 14 — the compiler reports where it noticed the problem, not where it is

Why: It tells you where the compiler was when it noticed a problem, which is not necessarily where the error is. Generally, the error will be prior to the location of the error message. A missing semicolon lets the compiler read on until something no longer makes sense.

14. Why fix one error at a time?

Explain it to yourself

It seems slower than fixing several.

Discussion prompt

The appendix recommends fixing one error and recompiling. Why is that faster than working through the list?

Hint: How many of the messages are real?

Answer:

Because most of the messages may not be real errors. Fixing them means changing code that was correct, which introduces genuine errors where there were none.

And the first fix changes everything after it. Recompiling after one edit gives you a fresh, accurate first message rather than a stale list.

It is incremental development applied to debugging — one change, one check. The instinct to batch the work is exactly what makes it take longer.

15. When nothing works

Section

Section D.1

16. Are you and the compiler looking at the same code?

Concept

If the compiler says there is an error and you don't see it, that might be because you and the compiler are not looking at the same code.

// the test:
// put an obvious, deliberate syntax error at the very
// top of the file, then compile again.

xyzzy!!!
public class Bob {
resultmeans
the compiler reports the new erroryou are editing the file it compiles
it does notsomething is wrong with your setup

This situation is often the result of having multiple copies of the same program. You might be editing one version of the file but compiling a different version. If you are not sure, try putting an obvious and deliberate syntax error right at the beginning of the program.

17. Debugging by bisection

Notation

If you have examined the code thoroughly, and you are sure the compiler is compiling the right source file, it is time for desperate measures.

Annotate

  • Back up first. The whole technique involves deleting code, and the backup is what makes that safe.
  • Each step halves the search space, which is Lesson 12b's binary search applied to a file.
  • A thousand-line file reaches one line in about ten steps — the same log₂ arithmetic.
  • This process is ugly, but it goes faster than you might think and is very reliable.
  • It works for other programming languages too! — and for configuration files, and for finding which commit broke something.

Bisection needs no understanding of the code at all. That is exactly why it works when everything else has failed: it replaces insight with a procedure that cannot get stuck.

18. Bisecting a file

Worked example

A 400-line file will not compile and the message makes no sense. Six steps find the line.

# step 1: copy Bob.java to Bob.java.old
# step 2: delete lines 200-400. compile.
steplines remainingcompiles?the error is in
11–200yes201–400
21–300no201–300
31–250no201–250
41–225yes226–250
51–238no226–238
6narrowing—one line

Delete half, compile.

Why: Whether it compiles tells you which half contains the error.

Keep the half that contains it.

Why: And halve again.

About log₂ 400 ≈ 9 steps.

Why: Fewer in practice, because the region stops being plausible sooner.

Then restore.

Why: Start bringing back the code you deleted, a little bit at a time.

Verify: Notice that step 1's answer is no error in the first half even though the first half might not be valid code on its own.

Why: Deleting half a file often creates new errors — an unclosed brace, a missing method. In practice you delete whole methods rather than arbitrary line ranges, which keeps each version syntactically plausible and the answers meaningful.

19. Why plant a deliberate syntax error?

Prediction

The compiler reports an error you cannot find.

// put xyzzy!!! at the top of the file and compile
the compilerconclusion
reports the new error?
does not?

Predict first

What does this test tell you?

  • Whether the compiler is reading the file you are editing
  • Whether the error is a syntax error
  • How many errors there are
  • Which line the error is on

Correct: Whether the compiler is reading the file you are editing

Why: If the compiler doesn't find the new error, there is probably something wrong with the way you set up the development environment. The usual cause is multiple copies of the same program — editing one file and compiling another, which no amount of staring at the code will reveal.

20. Bisection everywhere

Concept

It works for other programming languages too — and for a good deal more than languages.

what is brokenwhat you bisect
a file that will not compilethe lines of the file
a config that will not loadthe settings
a page that renders wronglythe elements
a program that used to workthe commits — git bisect
a sorted arraythe range — Lesson 12b

Every row is the same algorithm: split the search space, ask one yes-or-no question, discard half. Lesson 12b introduced it for finding a card; this is the same log₂ n saving applied to finding a mistake.

21. Bisecting without a backup

Trap

The trap

The technique deletes most of your code.

# delete lines 200-400. compile. delete lines 100-200.
# compile. delete lines 50-100...
# ...and now you cannot remember what was in any of them
without a backupconsequence
each step destroys codeirreversibly
after six stepsmost of the file is gone
finding the bugno longer helps

You will have located the error precisely, in a file that no longer contains your program. The procedure only works because the deletions are recoverable.

The fix

Make a backup first, every time.

# step 0, always:
cp Bob.java Bob.java.old

# now delete freely - the original is safe
with a backup
deleting halfsafe
restoringcopy back from .old
if you get loststart over

Make a backup of the file you are working on is step one of the appendix's own instructions, and it is not a formality. Better still, use version control — then every intermediate state is recoverable and the whole technique becomes routine.

22. How many bisection steps?

Prediction

A 1000-line file.

// each step halves the remaining code
remainingafter
1000start
5001 step
1?

Predict first

Roughly how many steps to reach one line?

  • About 10
  • About 500
  • About 1000
  • About 100

Correct: About 10

Why: Halving a thousand repeatedly reaches one in about ten steps, because log₂ 1000 is close to 10 — exactly the arithmetic from Lesson 12b's binary search. This process is ugly, but it goes faster than you might think and is very reliable.

23. The first step of bisection

Fill the middle

Before deleting anything.

Fill in the blanks

# 1. make a backup of the file
# 2. delete about half the code and compile

Why: The backup is what makes the deletions safe, and halving is what makes the search fast — about ten steps for a thousand lines. Whether the reduced version compiles tells you which half contains the error, with no understanding of the code required.

24. Where else does bisection work?

Real world

It works for other programming languages too!

Discussion prompt

The technique needs only a yes-or-no test and a divisible search space. Where else does that description fit?

Hint: A program that used to work.

Answer:

Version history. git bisect finds the commit that broke something by checking out the midpoint of a range and asking does it work here? — a hundred commits in about seven builds.

Configuration, data and input: comment out half the settings, feed in half the records, delete half a document. Anything with a reproducible failure and a splittable input.

And it needs no understanding at all, which is why it works precisely when you have run out of ideas. A procedure that cannot get stuck is worth more than insight you do not currently have.

25. Run-time errors: hanging

Section

Section D.2

26. The program stops and does nothing

Concept

If a program stops and seems to be doing nothing, we say it is hanging. Often that means it is caught in an infinite loop or an infinite recursion.

System.out.println("entering the loop");
while (...) {
    ...
}
System.out.println("exiting the loop");
you seeconclusion
both messagesthe loop is not the problem
only the firstit is stuck in the loop
neitherit never got that far

Run the program. If you get the first message and not the second, you know where the program is getting stuck. Two print statements, and they turn the program hangs into the program hangs here — which is most of the work.

27. Three questions, in order

Notation

The appendix gives a decision procedure, and the order matters.

Annotate

  • Start with a loop you suspect — cheapest to test, and most often the cause.
  • A StackOverflowError points at recursion, and is usually the clearest signal in the whole appendix.
  • If the program is slow, it may take a long time to fill the stack — so a hang can still be recursion even without the error yet.
  • You can still use the techniques in the infinite recursion section even without a StackOverflowError.
  • The last case is the humbling one: you might not understand the flow of execution in your program, which is different from the program being wrong.

Localise before you diagnose. All three branches start by finding out where the program is, and only then ask why.

28. Tracing an infinite loop

Worked example

If you think you have an infinite loop and you know which loop it is, add a print statement at the end of the loop that displays the values of the variables in the condition, and the value of the condition.

while (x > 0 && y < 0) {
    // do something to x
    // do something to y
    System.out.println("x: " + x);
    System.out.println("y: " + y);
    System.out.println("condition: " + (x > 0 && y < 0));
}
iterationxycondition
15-3true
24-3true
33-3true
…counting downnever changestrue

Print the variables in the condition.

Why: Both of them, every time round.

And print the condition itself.

Why: (x > 0 && y < 0) — evaluated, so you see what the loop sees.

Read the output.

Why: Now when you run the program, you see three lines of output for each time through the loop.

Look for what is not moving.

Why: The last time through the loop, the condition should be false. If the loop keeps going, you will see the values of x and y, and you might figure out why they are not getting updated correctly.

Verify: In the trace above, y never changes — so the second half of the condition can never become false.

Why: Printing the condition as well as the variables is the part people skip. A compound condition can be true for a reason you did not expect, and seeing the boolean itself removes the guesswork.

29. What does a StackOverflowError usually mean?

Prediction

The program ran for a while and then crashed.

Exception in thread "main" java.lang.StackOverflowError
    at Countdown.countdown(Countdown.java:7)
    at Countdown.countdown(Countdown.java:10)
    at Countdown.countdown(Countdown.java:10)
    ... repeated
the stack trace showsmeaning
one method, repeated?

Predict first

What is the likely cause?

  • Infinite recursion — a missing or unreachable base case
  • An infinite loop
  • An array index out of bounds
  • The program is too large

Correct: Infinite recursion — a missing or unreachable base case

Why: Most of the time, an infinite recursion will cause the program to run for a while and then produce a StackOverflowError. The repeated frames in the trace are the tell: each recursive call adds one, and the stack eventually runs out. Check for a base case, then check that the parameters are moving toward it.

30. Infinite recursion

Concept

If you know which method is causing an infinite recursion, check that there is a base case.

public void countdown(int n) {
    System.out.println("countdown(" + n + ")");   // print the parameters
    if (n == 0) {
        return;
    }
    countdown(n - 1);
}
checkwhat to look for
is there a base case?a condition that returns without recursing
is it reachable?print the parameters and watch them
are they moving toward it?if not, that is the bug

There should be a condition that makes the method return without making a recursive invocation. If not, you need to rethink the algorithm and identify a base case. If there is a base case, but the program doesn't seem to be reaching it, add a print statement at the beginning of the method that displays the parameters. Lesson 8a's two requirements, as a diagnostic.

31. Printing without knowing what you are looking for

Trap

The trap

Print statements everywhere, and no hypothesis.

System.out.println("here 1");
System.out.println("here 2");
System.out.println(x);
System.out.println("here 3");
System.out.println("in the loop");
// ...forty more
problemconsequence
no hypothesisyou do not know what would confirm it
unlabelled valueswhich x is that?
too much outputthe answer is in there somewhere

One of the problems with using print statements for debugging is that you can end up buried in output. Adding more is the instinct; it is usually the wrong direction.

The fix

One question at a time, labelled.

// hypothesis: this loop never exits because y is not updated
System.out.println("x: " + x);
System.out.println("y: " + y);
System.out.println("condition: " + (x > 0 && y < 0));
habitwhy
a hypothesis firstso you know what would disprove it
label every valuex: 5, not 5
print the condition toocompound conditions surprise you
remove them afterwardsLesson 12b's discipline

As you develop a program, you should write code to generate concise, informative traces of what the program is doing. A trace is a designed artefact, not an accumulation — which is the difference between debugging and flailing.

32. Which section of the appendix?

Definition probe

The symptom decides.

Sort into buckets

Sort each symptom.

infinite loop
prints the first message and not the second
infinite recursion
runs a while, then StackOverflowError
flow of execution
no idea which method is even running; hangs with no error, and no loop suspected
loop
Execution entered the loop and never left, which the two bracketing print statements establish.
rec
The stack filling up is the signature of a method calling itself without reaching a base case.
flow
When you cannot localise the problem at all, add a print statement at the start of each method and watch the trace.

33. Trace a suspect loop

Fill the middle

Print the variables and the condition.

Fill in the blanks

while (x > 0 && y < 0) x} > 0 && y < 0));
}

Why: Printing the condition itself, not just its variables, is the part people skip — a compound condition can stay true for a reason that is not obvious from the values alone. The last iteration should show the condition false; if it never does, one of the variables is not being updated.

34. Why print at the start of every method?

Socratic

Flow of execution, the third branch.

Discussion prompt

When you cannot localise a hang at all, the appendix suggests printing entering method foo in every method. What does that give you?

Hint: What does the output look like?

Answer:

A trace of each method as it is invoked — so you can see where the program actually got to, and where it stopped.

You can also display the arguments each method receives. When you run the program, check whether the values are reasonable, and check for one of the most common errors — providing arguments in the wrong order.

It is a hand-built call trace, and a debugger gives you the same thing without editing anything — which is Lesson A's argument for learning one. The print version works everywhere, including places a debugger cannot reach.

35. Reading an exception

Section

Section D.2

36. The message, the line, and the stack trace

Concept

When an exception occurs, Java displays a message that includes the name of the exception, the line of the program where the exception occurred, and a stack trace.

Exception in thread "main" java.lang.NullPointerException
    at Test.main(Test.java:7)
parttells you
the exception namewhat kind of thing went wrong
the line numberwhere it happened
the stack tracehow the program got there

The stack trace includes the method that was running, the method that invoked it, the method that invoked that one, and so on. It is Lesson 4a's stack diagram, printed at the moment of failure — and unlike a compiler message, the line number really is where it happened.

37. The five common exceptions

Notation

The first step is to examine the place in the program where the error occurred and see if you can figure out what happened.

Annotate

  • NullPointerException: figure out which variable is null and then figure out how it got to be that way — the second half is the real work.
  • Remember that when you declare a variable with an array type, its elements are initially null until you assign a value to them — Lesson 12b's array of objects.
  • ArrayIndexOutOfBoundsException: add a print statement before it showing the index and the length. Is the array the right size? Is the index the right value?
  • FileNotFoundException depends on your filesystem, so it can be hard to track down — check the path and, in a project-based IDE, whether the file was imported.
  • ArithmeticException on integers means division by zero; note that floating-point division by zero gives Infinity rather than an exception.

Every one of these appeared earlier in the book. The appendix's contribution is the second question for each: not what is null but how did it get to be null, which is where the actual bug lives.

38. Working backward from an index error

Worked example

Now work your way backward through the program and see where the array and the index come from.

System.out.println("index: " + i);
System.out.println("length: " + array.length);
System.out.println(array[i]);      // the exception happens here
stepquestion
at the failureis the index right? is the array the right size?
one step backfind the nearest assignment to each
if either is a parametergo to the call site
at the call sitewhere do those values come from?

Print both values at the point of failure.

Why: Add a print statement immediately before it to display the value of the index and the length of the array.

Decide which one is wrong.

Why: Is the array the right size? Is the index the right value? Usually only one of them is.

Follow it backward.

Why: Find the nearest assignment statement and see if it is doing the right thing.

Cross method boundaries if needed.

Why: If either one is a parameter, go to the place where the method is invoked and see where the values are coming from.

Verify: Notice this is a search backward through the program, one assignment at a time.

Why: The exception tells you where the wrong value was used, not where it was produced. Tracing back to the assignment that created it is the whole technique — and it is the same movement as looking before the line a compiler names.

39. What causes this NullPointerException?

Prediction

An array of objects.

int[] array = new Point[5];
System.out.println(array[0].x);
after new Point[5]elements
array[0]?

Predict first

Why does the second line throw?

  • The array's elements are initially null, so array[0] is not an object
  • The array has no elements
  • Point has no x field
  • The array type is wrong

Correct: The array's elements are initially null, so array[0] is not an object

Why: Remember that when you declare a variable with an array type, its elements are initially null until you assign a value to them. The array exists with five slots; no Point objects were created — which is Lesson 12b's array of nulls, met here as a diagnosis.

40. Finding the minimal test case

Concept

To simplify the program, scale down the problem the program is working on.

if the programtry
sorts an arraya small array
takes input from the userthe simplest input that causes the error
has a deeply nested partrewriting that part with a simpler structure
has a large methodsplitting it into smaller methods and testing them separately

The process of finding the minimal test case often leads you to the bug. For example, if you find that a program works when the array has an even number of elements, but not when it has an odd number, that gives you a clue about what is going on. The reduction is not preparation for debugging — it often is the debugging.

41. Drowning in output

Trap

The trap

More print statements, on a bigger input.

// sorting 10,000 elements, printing every comparison
// 100,000 lines of output, and the bug is in there somewhere
problemeffect
the input is largethe output is enormous
everything is printednothing stands out
scrollingyou lose the beginning

One of the problems with using print statements for debugging is that you can end up buried in output. The instinct when a trace is not helping is to add more of it, and that makes it worse.

The fix

Simplify the output, or simplify the program.

// simplify the output:
//   remove or comment out prints that aren't helping,
//   combine them, or format them so they are readable

// simplify the program:
//   sort a SMALL array
//   give the simplest input that causes the error
approachwhat it does
fewer, better printsthe trace becomes readable
a smaller inputthe trace becomes short
cleaner codethe bug becomes visible

There are two ways to proceed: either simplify the output or simplify the program. And there is a bonus: reorganizing the program can help you find subtle bugs. If you make a change that you think doesn't affect the program, and it does, that can tip you off.

42. Which exception?

Definition probe

Each names a different kind of mistake.

Sort into buckets

Sort each situation.

NullPointerException
calling a method on a variable that is null
ArrayIndexOutOfBoundsException
using index 10 on an array of length 5
ArithmeticException
integer division by zero
StackOverflowError
a recursive method with no base case
npe
There is no object to call the method on — the first task is finding which variable is null, and the second is finding how it got that way.
aioobe
The index is negative or greater than array.length - 1; print both the index and the length at the point of failure.
ae
Something went wrong during an arithmetic operation, division by zero being the usual case.
soe
The stack filled up, which almost always means recursion that never reaches a base case.

43. What does the stack trace tell you?

Prediction

It lists more than one method.

at Deck.subdeck(Deck.java:45)
at Deck.mergeSort(Deck.java:88)
at Main.main(Main.java:12)
linemeaning
the firstwhere it failed
the rest?

Predict first

What do the lower lines show?

  • The chain of calls that led there — mergeSort called subdeck, and main called mergeSort
  • Other errors that also occurred
  • Methods that will run next
  • Lines that were skipped

Correct: The chain of calls that led there — mergeSort called subdeck, and main called mergeSort

Why: The stack trace includes the method that was running, the method that invoked it, the method that invoked that one, and so on. It answers how did we get here, which for a method called from several places is often the whole question — Lesson 4a's stack diagram, printed automatically.

44. Why does a smaller test case help?

Real world

The bug is the same either way.

Discussion prompt

The process of finding the minimal test case often leads you to the bug. Why would shrinking the input reveal anything?

Hint: What can you check by hand?

Answer:

Because a small case can be traced by hand. Five elements you can work through on paper; ten thousand you cannot, so you are reduced to guessing.

And the boundary between working and failing is informative. Works with an even number of elements, fails with an odd one is nearly a diagnosis on its own.

Shrinking is a form of bisection — you are narrowing the space in which the bug can hide, the same move as deleting half a file, applied to the input instead of the code.

45. Logic errors, and being stuck

Section

Section D.3

46. Only you know it is wrong

Concept

Logic errors are hard to find because the compiler and interpreter provide no information about what is wrong. Only you know what the program is supposed to do, and only you know that it isn't doing it.

// Is there something the program was supposed to do that
//   doesn't seem to be happening?
// Is something happening that shouldn't?
// Is a section of code producing an unexpected effect?
questionwhat to do
something is not happeningfind that code, check it is executing
something happens that should notfind that code, see if it runs when it should not
a section behaves unexpectedlyread the documentation for the methods it calls

The first step is to make a connection between the code and the behavior you get. You need a hypothesis about what the program is actually doing. Not what it should do — what it is doing, which is a different and more useful question.

47. The problem might be in your head

Notation

The appendix's most striking sentence, and the technique that follows from it.

Annotate

  • A logic error is a disagreement between what you believe and what the machine does. One of the two is wrong, and it is not always the machine.
  • Make sure you understand the code, especially if it invokes methods in the Java library — they might not do what you think they do.
  • Read the documentation for those methods, and try them out with simple test cases — Lesson B's skill, used as a debugging technique.
  • Testing components independently is unit testing — Lesson A's JUnit, motivated from the other direction.
  • Inheritance can lead to subtle logic errors, because you can run inherited code without realizing it — Chapters 14 to 17 made that possible.

Once you find the discrepancy between your model and reality, you can solve the problem. Finding it is the work; the fix is usually easy afterwards.

48. The list of common logic errors

Worked example

Here are some common logic errors to check for. Every one appeared earlier in the book.

// integer division always rounds toward zero
//   -> use double for fractions; integers for countable
//      things, floating-point for measurable things

// floating-point numbers are only approximate
//   -> never use == with doubles
//      if (Math.abs(d - 1.23) < .000001)

// == on objects checks IDENTITY
//   -> use equals for equivalence

// by default, equals checks identity too
//   -> override it if you want a different notion

// inheritance can run code you did not realise was there
errorthe lesson
integer division2b
comparing doubles with ==11b
== on objects9a and 11b
the default equals11b
inherited code running14a and 17b

Integer division rounds toward zero.

Why: Use integers for countable things and floating-point numbers for measurable things — a rule worth more than the arithmetic.

Doubles are approximate.

Why: You should probably never use the == operator with doubles. Compare with a tolerance.

== on objects asks the wrong question.

Why: If you meant to check equivalence, you should use the equals method instead.

And equals defaults to identity.

Why: If you want a different notion of equivalence, you have to override it.

Verify: Count how many of the five you have made at least once while working through this course.

Why: These are not obscure edge cases. They are the five mistakes that produce a program which compiles, runs, and quietly gives the wrong answer — which is why they get their own list.

49. What does x / 2 * Math.PI compute?

Prediction

Same precedence, left to right.

double y = x / 2 * Math.PI;
operatorprecedence
/same as *
*same as /

Predict first

How is it grouped?

  • (x / 2) × π
  • x / (2 × π)
  • It depends on the value of x
  • It does not compile

Correct: (x / 2) × π

Why: Multiplication and division have the same precedence, and they are evaluated from left to right. So the division happens first, and the result is π² times too large. Nothing reports it — which is the definition of a logic error, and why parentheses are worth the two characters.

50. Break up big expressions

Concept

Writing complex expressions is fine as long as they are readable, but they can be hard to debug.

double halfWidth = 0.5 * rect.getWidth();
double halfHeight = 0.5 * rect.getHeight();
int dx = (int) Math.round(halfWidth);
int dy = (int) Math.round(halfHeight);
rect.translate(dx, dy);
one expressiontemporary variables
readablelessmore — the names document it
can you print an intermediate value?noyes
can you check the types?hardthey are written down
where did it go wrong?somewhere in thereone line

The second version is easier to read, partly because the variable names provide additional documentation. It's also easier to debug, because you can check the types of the temporary variables and display their values. The same argument applies to a complex return: now you have the opportunity to display any of the intermediate variables before returning.

51. Trusting operator precedence you have not checked

Trap

The trap

Multiplication and division have the same precedence.

// to evaluate x / (2 pi):
double y = x / 2 * Math.PI;      // WRONG - this is (x/2) * pi

double y = x / (2 * Math.PI);    // correct
expressionevaluates aswanted
x / 2 * Math.PI(x / 2) × πx / (2π)
whysame precedence, left to right—
does it compile?yes—

That is not correct, because multiplication and division have the same precedence, and they are evaluated from left to right. The program runs and produces a number that is wrong by a factor of π² — a logic error of exactly the kind nothing reports.

The fix

Use parentheses, even when you are sure.

double y = x / (2 * Math.PI);
benefitdetail
correctthe grouping is explicit
readableno memorisation required
cheaptwo characters

This version is correct, and more readable for other people who haven't memorized the order of operations. If you are not sure of the order of operations, check the documentation, or use parentheses to make it explicit — and the parentheses are the better answer even when you are sure.

52. Which logic error?

Definition probe

Five that compile and run.

Sort into buckets

Sort each symptom.

integer division
5 / 2 gives 2, not 2.5
floating-point approximation
two equal-looking doubles are not ==
== on objects checks identity
two Cards representing the same card are not ==
inherited code
a method you did not write ran
div
Integer division always rounds toward zero — use double when you want fractions.
fp
Floating-point numbers are only approximate, so compare with a tolerance rather than ==.
obj
== asks whether two references point at the same object; equals asks whether they represent the same value.
inh
Inheritance can lead to subtle logic errors, because you can run inherited code without realizing it.

53. Compare doubles safely

Fill the middle

Never with ==.

Fill in the blanks

if (Math.abs(d - 1.23) < .000001) ___

Why: The absolute value of the difference is compared against a small tolerance, because floating-point values are approximate and two calculations that should agree often differ in the last bits. Lesson 11b used exactly this pattern for a Time class's equals method.

54. Does the rubber duck really work?

Real world

We're not kidding, it works!

Discussion prompt

The appendix recommends explaining your problem to a rubber duck, in earnest. Why would talking to an inanimate object help?

Hint: What does explaining require?

Answer:

Because explaining forces you to be explicit. You have to say what the code is supposed to do, what it actually does, and why you believed those were the same — and the gap usually appears while you are saying it.

By the time you explain the problem to someone, you might see the answer. The appendix says this is so common it has a name, and the duck is just a listener who is always available.

It also stops you skipping steps. Silent thinking glosses over the assumption you never questioned; saying it out loud does not — which is why the technique survives being faintly ridiculous.

55. The three kinds of error

Comparison

Fill the blanks.

Comparison matrix

compile-timerun-timelogic
what is wrongthe syntaxsomething fails while runningthe program does the wrong thing
who tells youthe compilerthe program, with an exceptionnobody
examplea missing semicolonStackOverflowErroran expression evaluated in the wrong order
first moveread the first message, look before that lineread the stack traceform a hypothesis about what it is doing
hardest becausemessages can be spuriousthe cause may be far from the symptomonly you know it is wrong

The bottom-right cell is why the third column gets its own section. A compiler error stops you; a logic error lets you carry on being wrong — sometimes for a long time.

56. The pattern to carry away

Pattern

Localise, then diagnose — and halve the search space whenever you can.

// 1. which kind of error is it?
//      compile-time -> read the FIRST message, look BEFORE that line
//      run-time     -> read the stack trace
//      logic        -> form a hypothesis about what it IS doing

// 2. localise before diagnosing
System.out.println("entering the loop");
...
System.out.println("exiting the loop");

// 3. halve the search space
//      the file  -> bisection
//      the input -> the minimal test case
//      the code  -> temporary variables

// 4. when stuck: walk away, then explain it out loud
techniqueanswers
bracketing print statementswhere did it stop?
printing the loop conditionwhy is it still true?
printing the parametersare they reaching the base case?
bisectionwhich half contains it?
the minimal test casewhat exactly triggers it?
temporary variableswhich sub-expression is wrong?

57. Check: compiler messages

Check

Work it out before you click.

// the compiler reports 100 errors
messagereliable?
the firstyes
the others?

Check your understanding

What should you do?

  • A. Fix only the first error and recompile — the rest may be spurious (correct)
  • B. Fix all 100 before recompiling
  • C. Start from the last message, which is closest to the end
  • D. Rewrite the program

Answer: A

Why: Only the first error message is truly reliable. We suggest that you fix only one error at a time and then recompile the program. You may find that one semicolon or brace fixes 100 errors. Working through the whole list means changing code that was correct, which creates real errors where there were none.

Why B tempts people
Most of them may not be errors; the compiler got thrown off track after the first.
Why C tempts people
The later messages are the least trustworthy, being furthest from the real problem.
Why D tempts people
A hundred messages commonly come from one missing character.

58. Check: a hanging program

Check

Work it out before you click.

// the program runs for a few seconds, then:
Exception in thread "main" java.lang.StackOverflowError
    at Series.fibonacci(Series.java:8)
    at Series.fibonacci(Series.java:8)
    ... repeated
the trace showsmeaning
one method repeated?

Check your understanding

What is the likely cause, and what do you check first?

  • A. Infinite recursion — check that there is a base case and that the parameters move toward it (correct)
  • B. An infinite loop — add print statements around the loop
  • C. The array is too large
  • D. A null reference

Answer: A

Why: Most of the time, an infinite recursion will cause the program to run for a while and then produce a StackOverflowError. The two checks are Lesson 8a's requirements: check that there is a base case, and if there is one but the program is not reaching it, add a print statement at the beginning of the method that displays the parameters and watch whether they are moving toward it.

Why B tempts people
A loop does not consume stack frames; the repeated frames in the trace point at recursion.
Why C tempts people
An oversized array gives an OutOfMemoryError, and would not repeat one method.
Why D tempts people
That would be a NullPointerException, named as such.

59. Check: a logic error

Check

Work it out before you click.

double y = x / 2 * Math.PI;
// intended: x / (2 * pi)
grouping
as written?

Check your understanding

What is wrong, and how do you fix it?

  • A. * and / have the same precedence and evaluate left to right, so it computes (x/2)×π — add parentheses (correct)
  • B. Math.PI must be cast to double
  • C. Integer division truncates the result
  • D. Nothing is wrong

Answer: A

Why: That is not correct, because multiplication and division have the same precedence, and they are evaluated from left to right. The fix is x / (2 * Math.PI) — and the appendix's advice generalises: if you are not sure of the order of operations, check the documentation, or use parentheses to make it explicit, which is also more readable for anyone who has not memorised the rules.

Why B tempts people
Math.PI is already a double; no cast is involved.
Why C tempts people
x is a double, so the division is floating-point — the grouping is the problem, not truncation.
Why D tempts people
The result is wrong by a factor of π², silently.

60. The part about getting up and walking away

Real world

First, get away from the computer for a few minutes. Computers emit waves that affect the brain, causing the following symptoms: frustration and rage; superstitious beliefs; sour grapes.

Discussion prompt

The appendix is joking about the waves and serious about the advice. Why does stepping away actually help?

Hint: What happens to your thinking after an hour of failure?

Answer:

Because frustration narrows what you consider. After an hour you are re-checking the same three lines and have stopped questioning the assumption that is actually wrong.

Sometimes it just takes time to find a bug. People often find bugs when they let their mind wander. Good places to find bugs are buses, showers, and bed. That is a widely reported experience, not a joke.

And the symptoms it lists are real diagnostic signs. The computer hates me and this program is lame anyway are both ways of saying you have stopped debugging — which is precisely when a walk is more productive than another hour.

61. How sure are you?

Commit first

Commit to an answer and to your confidence.

Predict first

The compiler names line 42 as the site of a syntax error. Where should you look?

  • At line 42 and, especially, before it — the compiler reports where it noticed the problem
  • Only at line 42 — the compiler is precise
  • After line 42
  • In whichever method line 42 calls

Correct: At line 42 and, especially, before it — the compiler reports where it noticed the problem

Why: Actually, it tells you where the compiler was when it noticed a problem, which is not necessarily where the error is. Generally, the error will be prior to the location of the error message, but in some cases it will be somewhere else entirely. A missing semicolon lets the compiler keep reading until something no longer fits, which may be a line or two later. The fourth option is not absurd — the appendix notes that if you get an error message at a method invocation, the actual error may be in the method definition itself — but look earlier is the general rule, and broaden the search is the fallback.

62. Explain it to someone else

Explain it

Two minutes, out loud — and it is also the technique.

Discussion prompt

A classmate says my program doesn't work and does not know where to start. Give them the first three questions to answer.

Hint: The kind of error decides everything else.

Answer:

First: which kind of error is it? Does it fail to compile, crash while running, or run fine and give the wrong answer? Each has a completely different strategy.

*Second: where is it? If it compiles, read the stack trace or bracket a suspect section with print statements. Turning it doesn't work into it stops here is most of the job.*

Third: what is the smallest input that still fails? Shrinking the test case often finds the bug on its own. And then explain the whole thing out loud — the appendix means the rubber duck seriously, and by the time you have finished explaining you often see it.

63. Exit ticket

Exit ticket

One question before you close the deck — and the course.

Predict first

Why does the best debugging strategy depend on the kind of error?

  • Because each kind gives you different information: the compiler names a location, an exception gives a stack trace, and a logic error reports nothing at all
  • Because logic errors are always in the main method
  • Because run-time errors cannot be fixed
  • Because compile-time errors are always the first to appear

Correct: Because each kind gives you different information: the compiler names a location, an exception gives a stack trace, and a logic error reports nothing at all

Why: The best debugging strategy depends on what kind of error you have. A compile-time error hands you a line number — approximate, and before it rather than at it. A run-time error hands you an exception name, a line, and a stack trace showing how you got there. A logic error hands you nothing, because only you know what the program is supposed to do, and only you know that it isn't doing it — which is why that section starts by asking you to form a hypothesis about what the program is actually doing, and why it warns that the problem might not actually be the program; it might be in your head.

64. Draw the whole lesson

Connect it up

One page, from memory — the last one.

Draw it

Draw three columns headed compile-time, run-time and logic. Under each, write what is wrong, who tells you, one example, and the first move. Then, under run-time, add the five common exceptions with a one-line cause for each. Under compile-time, write the bisection procedure in four steps, starting with the backup. Under logic, list the five common errors — integer division, doubles with ==, objects with ==, the default equals, and inherited code. Finish by writing the four things to tell someone when you ask for help.

65. Recap

Recap

The last appendix, and the last lesson: everything the book has said about debugging, in one place.

if you remember one thingit is this
about strategywhich kind of error is it?
about methodlocalise before you diagnose
about being stuckexplain it out loud; the gap appears as you speak

Sources

  1. Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter D (Debugging), Sections D.1-D.3, pp. 327-340
  2. The Java Tutorials — What Is an Exception?
  3. Think Java 2e — free online edition and source code

Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108