Learn Java

Lesson 9Phase 1: First steps

Reading errors and debugging

Compile errors, runtime exceptions and logic bugs, how to read a stack trace, and a calm method for finding any bug.

By Updated 45 min

  • compile error
  • runtime error
  • logic error
  • stack trace
  • print debugging
  • breakpoints

Every programmer writes code that does not work. Beginners do, and so do people with twenty years of Java behind them. What experienced programmers have is a routine for finding the problem, and the calm to follow it. This lesson gives you that routine: how to read what Java tells you when something goes wrong, and what to do when it tells you nothing at all.

By the end of this lesson
  • Tell a compile error, a runtime error and a logic error apart, and say when each one shows up.
  • Read a javac message: the file, the line, the caret, and why you fix the first error first.
  • Read a stack trace from the top and find the line of your own code that crashed.
  • Track down a logic error by predicting values and checking them with labelled prints or a step-through.
  • Use a debugger's breakpoints, step over, step into, step out and watches.
  • Work through a calm checklist whenever a program misbehaves.
From before
You have met a few bugs by name already. Loops showed the off-by-one mistake and the loop that never ends. In one-dimensional arrays a loop stepped off the end of an array and Java stopped the program. Operators and casting explained why 7 / 2 is 3, and if and else warned about writing = where you mean ==. This lesson turns those into one method for finding any bug, using only Phase 1 tools: variables, operators, decisions, loops, arrays and ArrayList.

Three kinds of problem#

A mistake in a program is called a bug, and finding and fixing it is called debugging. Bugs come in three kinds. Picture a cook working from a recipe card, and three ways the card can let them down.

  • The card is so garbled that the cook will not even start: a sentence stops halfway, or a step uses an ingredient the card never listed. Nothing gets cooked.
  • The cook starts, follows the steps, and reaches one that cannot be done: “crack the fourth egg”, when the card only asked for three. Cooking stops right there, halfway through.
  • The cook follows every step perfectly and serves the dish, but the card said a cup of salt where it meant a teaspoon. No step failed, and the dish still comes out wrong.

Java programs fail in the same three ways.

KindWhen you find outWhat you seeExample
Compile errorBefore the program runs, while javac reads itMessages from javac, and no program to runA missing semicolon
Runtime errorWhile the program runsThe program stops and prints a reportAsking for item 3 of an array that holds 3 items
Logic errorWhen you notice the result is wrongNothing unusual: the program ends normallyA total that skips the first item
The three kinds of problem, from the one Java catches earliest to the one it cannot catch at all.
Key idea

Compile errors stop a program before it starts. Runtime errors stop it partway through. Logic errors never stop it, so you have to catch them yourself, by knowing what the right answer should be.

Java does a lot of the work for you with the first two kinds: it says what went wrong and on which line. With a logic error, Java has no idea anything is wrong, because the program did exactly what you wrote. Most of debugging is finding the gap between what you wrote and what you meant.

Quick check
A program should print the average of five test scores. It compiles, runs to the end and prints Average: 59.0, but the right answer is 77.4. What kind of problem is this?

Compile errors: Java refuses to start#

In your first Java program you saw that javac, the compiler, turns your .javafile into bytecode before anything runs. On the way it checks that every line follows Java's grammar and that every value fits its type. If something does not, it prints what it found, makes no .class file, and stops. That is a compile error.

Here is a program with one character missing. Try to spot it before you read what javac says.

Main.java
read along
1public class Main {
2 public static void main(String[] args) {
3 int apples = 5;
4 int pears = 3
5 int fruit = apples + pears;
6 System.out.println("Fruit: " + fruit);
7 }
8}
Terminal
read along
1$ javac Main.java
2Main.java:4: error: ';' expected
3 int pears = 3
4 ^
51 error

Reading the message

Every message from javac has the same parts, so once you can read one you can read them all.

  • Main.java is the file. In a project with many files, it tells you which one to open.
  • 4 is the line number. Editors show line numbers down the left edge of the code.
  • error: means the file did not compile. Messages marked warning: point at something suspicious but still let the program compile.
  • ';' expected is the message itself: what javac wanted to see and did not.
  • Below that, javac copies the line and puts a caret, the ^ symbol, under the spot where it got stuck.
  • 1 error at the bottom counts the errors in the whole file.

The caret sits just after the 3, where the semicolon belongs. javac read int pears = 3, then met int on the next line while it was still waiting for the statement to end.

If the reported line looks fine, look above it
javac reports where it noticed the problem, which is usually the line with the mistake. When it is not, the mistake is almost always a little earlier: a missing semicolon, quote or bracket only becomes a problem when Java reaches the code that follows it.

cannot find symbol

This is the error you will see most. A symbolis any name in your code: a variable, a method, a class. “Cannot find symbol” means you used a name Java does not know, at least not in that spot.

Main.java
read along
1public class Main {
2 public static void main(String[] args) {
3 int score = 42;
4 System.out.println("Score: " + scor);
5 }
6}
Terminal
read along
1$ javac Main.java
2Main.java:4: error: cannot find symbol
3 System.out.println("Score: " + scor);
4 ^
5 symbol: variable scor
6 location: class Main
71 error

The two extra lines say which name Java could not find (variable scor) and where it was looking (class Main). The usual causes are a typo like this one, different capital letters (to Java, Total and total are two different names), a variable used outside the braces it was declared in, and a variable that was never declared at all.

The same message turns up for methods and classes, and the symbol: line says which kind of name went wrong:

You wroteThe symbol line namesThe fix
System.out.printn("Hello");method printn(String)A typo in a method name. It is println.
string name = "Ana";class stringClass names start with a capital letter: String.
A loop's i, used after the loopvariable iA variable declared in a for line exists only inside that loop. Declare it before the loop if you need it afterwards.

incompatible types

Every variable has a type, and javac checks that each value you store fits it, as you saw in variables and types. These two versions of the error are the ones you will meet most. The line javac copies is all you need to see:

Terminal
read along
1$ javac Main.java
2Main.java:3: error: incompatible types: possible lossy conversion from double to int
3 int price = 4.99;
4 ^
51 error

4.99 is a double. Putting it in an int would throw away the .99, which is what “lossy” means, and Java will not do that unless you tell it to. Usually the fix is to make the variable a double. If you really want only the whole part, say so with a cast, (int) 4.99, from operators and casting.

Terminal
read along
1$ javac Main.java
2Main.java:3: error: incompatible types: String cannot be converted to int
3 int age = "12";
4 ^
51 error

The quotes make "12" a String, a piece of text, even though it looks like a number, and text never turns into an int by itself. Drop the quotes. When the text really comes from outside the program, convert it with Integer.parseInt, which appears later in this lesson.

missing return statement

This one comes from a tool you have not built yet. A method is a named block of code that other code can call; main is one, and some methods hand a value back with return. You will write your own in methods, and this message is waiting for you there, so here is a preview. grade is meant to hand back "pass" or "fail" for a score:

Main.java
read along
1public class Main {
2 public static void main(String[] args) {
3 System.out.println(grade(72));
4 }
5
6 static String grade(int score) {
7 if (score >= 50) {
8 return "pass";
9 } else if (score < 50) {
10 return "fail";
11 }
12 }
13}
Terminal
read along
1$ javac Main.java
2Main.java:12: error: missing return statement
3 }
4 ^
51 error

The line static String grade(int score) promises a String back every time. You can see that a score is either at least 50 or below 50, so one of the two return lines always runs. javac does not work out values like that. It sees an if and an else if, allows for both conditions being false, and finds a way to reach the closing brace on line 12 without returning anything. The caret points at that brace. A plain else fixes it, because javac knows an else runs whenever the if does not.

reached end of file while parsing

Main.java
read along
1public class Main {
2 public static void main(String[] args) {
3 int[] scores = {72, 85, 90};
4 for (int i = 0; i < scores.length; i++) {
5 System.out.println(scores[i]);
6
7 }
8}
Terminal
read along
1$ javac Main.java
2Main.java:8: error: reached end of file while parsing
3}
4 ^
51 error

Parsing is javacreading your code to work out its structure: which statements sit inside which braces. “Reached end of file while parsing” means the file ended while something was still open. Every { needs a matching }, and the for loop's closing brace used to be on line 6. The indentation suggests that line 7 closes main, but Java ignores indentation and pairs each } with the most recent { that is still open. So line 7 closes the loop, line 8 closes main, and the class is still open when the file runs out.

Let your editor count braces
Editors mark the matching bracket when your cursor sits next to one, and most can re-indent a whole file for you. After re-indenting, the place where the indentation suddenly looks wrong is usually next to the missing or extra brace.

Fix the first error first

One mistake can produce several messages. Here a variable was misspelled where it was declared:

Main.java
read along
1public class Main {
2 public static void main(String[] args) {
3 int[] prices = {4, 10, 6};
4 int totl = 0;
5 for (int i = 0; i < prices.length; i++) {
6 total = total + prices[i];
7 }
8 System.out.println("Total: " + total);
9 }
10}
Terminal
read along
1$ javac Main.java
2Main.java:6: error: cannot find symbol
3 total = total + prices[i];
4 ^
5 symbol: variable total
6 location: class Main
7Main.java:6: error: cannot find symbol
8 total = total + prices[i];
9 ^
10 symbol: variable total
11 location: class Main
12Main.java:8: error: cannot find symbol
13 System.out.println("Total: " + total);
14 ^
15 symbol: variable total
16 location: class Main
173 errors

One mistake caused all three errors. Line 4 declares totl, so every later total is a name Java has never heard of. None of the messages points at line 4, where the real mistake is, but the first one still leads you there: Java cannot find total, and asking “where did I declare total?” takes you straight to line 4. Correct it and all three messages disappear.

Sometimes the extra messages are stranger. This program is missing a single { at the end of line 4:

Main.java
read along
1public class Main {
2 public static void main(String[] args) {
3 int[] scores = {72, 85, 90};
4 for (int i = 0; i < scores.length; i++)
5 System.out.println(scores[i]);
6 }
7 System.out.println("Done");
8 }
9}
Terminal
read along
1$ javac Main.java
2Main.java:7: error: <identifier> expected
3 System.out.println("Done");
4 ^
5Main.java:7: error: illegal start of type
6 System.out.println("Done");
7 ^
8Main.java:9: error: class, interface, annotation type, enum, record, method or field expected
9}
10^
113 errors

None of the three messages mentions line 4. Without its {, the loop body is only line 5, so the } on line 6 closes main early. Java then reads line 7 as if it sat directly inside the class, where a statement such as println is not allowed. The }on line 8 closes the class, and the one on line 9 has nothing left to close. Each message is true from Java's point of view, and each one comes from the single missing brace.

Key idea

Fix the first error, then compile again. Later messages are often side effects of the first one, and one fix can clear a whole list.

The opposite happens too: fixing an error can reveal a new one. javac works in stages, and when the grammar is broken it stops before it checks types. This program has a missing semicolon on line 4 and a type mistake on line 6:

Main.java
read along
1public class Main {
2 public static void main(String[] args) {
3 int apples = 5;
4 int pears = 3
5 int fruit = apples + pears;
6 String label = fruit;
7 System.out.println(label);
8 }
9}

The first compile reports only the semicolon:

Terminal
read along
1$ javac Main.java
2Main.java:4: error: ';' expected
3 int pears = 3
4 ^
51 error

Add the semicolon, compile again, and the second mistake appears:

Terminal
read along
1$ javac Main.java
2Main.java:6: error: incompatible types: int cannot be converted to String
3 String label = fruit;
4 ^
51 error

A new error after a fix means javac got further through your file. That counts as progress, even when the number of errors stays the same.

The Run buttons here check less than javac
The engine behind the Run buttons in these notes is an interpreter, and it does not check types before it runs. It stops on broken grammar such as a missing semicolon, but it would run int price = 4.99; and quietly keep the 4, a program javac refuses. Every compile error on this page comes from the real javac, so these are the messages you will see on your own machine.
Quick check
javac reports four errors, on lines 6, 9, 12 and 15. What should you do first?

Runtime errors: the program crashes#

A program can compile cleanly and still hit a wall while it runs. Some things Java can only check at the moment they happen: whether an index is inside the array, whether the number you divide by is zero, whether a variable really refers to something. When one of those checks fails, Java throws an exception: it packs up a description of the problem, called an exception, and abandons whatever it was doing. If nothing catches the exception, the program ends. (Catching them is a later topic.)

Press Run. When the run stops, use the back arrow to step backwards from the crash and look at i just before it happened.

Main.java
1public class Main {
2 public static void main(String[] args) {
3 String[] players = {"Ana", "Ben", "Chen"};
4 for (int i = 0; i <= players.length; i++) {
5 System.out.println("Player " + i + ": " + players[i]);
6 }
7 System.out.println("Everyone is ready");
8 }
9}
Prints
›Player 0: Ana
›Player 1: Ben
›Player 2: Chen
!Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
! at Main.main(Main.java:5)
Stops on line 5

ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3

Array positions run from 0 up to length - 1. This index fell outside that range.

The three players print, then the program stops on line 5, and Everyone is ready never appears: once an exception goes uncaught, nothing after it runs. The lines marked ! in the output are the report Java prints. They go to standard error, a second output channel meant for problems, which editors and terminals can show apart from normal output.

Reading a stack trace, top down

The report is called a stack trace. Here it is as plain text:

Terminal
read along
1Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
2 at Main.main(Main.java:5)
  1. 1Exception in thread "main" says the program's main line of work crashed. A threadis Java's word for one line of work, and every program you have written so far has just one, called main.
  2. 2java.lang.ArrayIndexOutOfBoundsException names the kind of problem. The java.lang. part is the package it belongs to, a named group of Java's own classes (more on those in packages and imports). The name on its own often tells you what happened.
  3. 3After the colon comes the message, Index 3 out of bounds for length 3: the code asked for index 3 of an array whose length is 3, so its last index is 2.
  4. 4Each line that starts with at is a place in the code. at Main.main(Main.java:5) reads as class Main, method main, file Main.java, line 5. That is the line to open.

When the bad index equals the length, as it does here, the loop almost certainly used <= where it needed <.

Dividing a whole number by zero

Main.java
1public class Main {
2 public static void main(String[] args) {
3 int slices = 12;
4 int friends = 0;
5 System.out.println("Sharing " + slices + " slices");
6 int each = slices / friends;
7 System.out.println("Each friend gets " + each);
8 }
9}
Prints
›Sharing 12 slices
!Exception in thread "main" java.lang.ArithmeticException: / by zero
! at Main.main(Main.java:6)
Stops on line 6

ArithmeticException: / by zero

Whole numbers cannot be divided by zero. Check the divisor before dividing.

This time the report reads ArithmeticException: / by zero. Dividing a whole number by zero has no answer, so Java stops on line 6. The fix belongs before the division: make sure the divisor cannot be zero, for example with an if (friends > 0) around it.

Using null as if it were something

A String variable can hold null, the special value that means “nothing here”, which you met in variables and types. A new String[] array starts with null in every slot, the way a new int[] starts with zeros. Asking null for its length, or for anything else, is impossible, and Java throws a NullPointerException.

Main.java
1public class Main {
2 public static void main(String[] args) {
3 String[] names = new String[3];
4 names[0] = "Ana";
5 names[1] = "Ben";
6 String last = names[2];
7 System.out.println("The last name is " + last);
8 System.out.println("It has " + last.length() + " letters");
9 }
10}
Prints
›The last name is null
!Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "last" is null
! at Main.main(Main.java:8)
Stops on line 8

NullPointerException: Cannot invoke "String.length()" because "last" is null

A variable held null, so there was no object to use.

Notice two things. Printing null works: joined to a String, it turns into the text null, which is a clue in itself. What crashes is calling a method on it, on line 8. And the message says exactly what happened: Cannot invoke "String.length()" because "last" is null. It names the call that failed and the variable that held null. Messages this detailed arrived in Java 14 and have been switched on by default since Java 15.

Text that is not a number

Programs often receive numbers as text, typed into a box or read from a file. Integer.parseInt reads a whole number out of a String: Integer.parseInt("42") gives the int 42. When the text is not a whole number, it throws a NumberFormatException. In this list someone typed the letter o where they meant a zero:

Main.java
1public class Main {
2 public static void main(String[] args) {
3 String[] typed = {"12", "7", "4o"};
4 int total = 0;
5 for (int i = 0; i < typed.length; i++) {
6 total = total + Integer.parseInt(typed[i]);
7 System.out.println("Running total: " + total);
8 }
9 }
10}
Prints
›Running total: 12
›Running total: 19
!Exception in thread "main" java.lang.NumberFormatException: For input string: "4o"
! at Main.main(Main.java:6)
Stops on line 6

NumberFormatException: For input string: "4o"

The text was not a number Java could read, such as "abc" or "12.5" for an int.

The message, For input string: "4o", shows the text it could not read inside quotes, so even a stray space would be visible. The two running totals printed first tell you the first two values were fine.

Which line is yours?

In the browser that trace has a single at line. Compile and run the same program in a terminal, and real Java prints more:

Terminal
read along
1$ javac Main.java
2$ java Main
3Running total: 12
4Running total: 19
5Exception in thread "main" java.lang.NumberFormatException: For input string: "4o"
6 at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
7 at java.base/java.lang.Integer.parseInt(Integer.java:527)
8 at java.base/java.lang.Integer.parseInt(Integer.java:624)
9 at Main.main(Main.java:6)

The at lines list the methods that were running at the moment of the crash, newest first, so the method on each line was called by the method on the line below it. Read from the bottom up, they tell the story: main, on line 6, called Integer.parseInt. That method called a second version of parseInt, which called forInputString, which built the exception.

Lines that start with java.base/are inside Java's own library. That code has been used and tested for decades, so the bug is almost never there. Skip down to the first line that names your own file, here Main.java:6: the line that handed "4o" to parseInt.

Your numbers may differ
The line numbers inside Java's files, like Integer.java:527, change from one Java version to the next, so your terminal may show different ones. The browser leaves the library lines out and shows only the lines in your code.
Key idea

Read a stack trace from the top. The first line says what went wrong. The first at line that names your own file says where to look.

These are the runtime errors you will meet most often in Phase 1 programs:

ExceptionWhat went wrongA typical cause
ArrayIndexOutOfBoundsExceptionAn array index that is negative, or the length or bigger<= in a loop condition, or a size typed in by hand
IndexOutOfBoundsExceptionThe same for an ArrayList: a position outside 0 to size() - 1list.get(list.size())
ArithmeticExceptionA whole number divided by zero, with / or %A count or divisor that can be 0
NullPointerExceptionUsing null as if it were a String, array or listA variable or array slot that was never given a value
NumberFormatExceptionText that is not a whole number given to Integer.parseIntLetters, spaces or a decimal point, as in "12.5"
Catching an exception
An exception does not have to end the program. In exception handling you will learn to catch one and recover, for example by asking the user to type the number again. Until then, treat each exception as a bug to fix in the code that caused it.
Quick check
In the NumberFormatException trace above, which line should you open first to fix the bug?

Logic errors: it runs, but the answer is wrong#

A logic error comes with no message, no line number and no stack trace. The program runs to the end and prints something wrong, or does the wrong thing. The only alarm is you noticing that the result does not match what you expected, so you need to know the right answer first. Work it out by hand for a small example before you trust the program's answer.

Off by one, and other wrong bounds

In loops the off-by-one mistake made a loop run once too often or once too few. With arrays it often does not crash at all. This program should add up a week of step counts, and by hand 4200 + 6100 + 3900 + 8000 is 22200:

Main.java
1public class Main {
2 public static void main(String[] args) {
3 int[] steps = {4200, 6100, 3900, 8000};
4 int total = 0;
5 for (int i = 1; i < steps.length; i++) {
6 total = total + steps[i];
7 }
8 System.out.println("Steps this week: " + total);
9 }
10}
Prints
›Steps this week: 18000

It prints Steps this week: 18000. The loop starts at 1, so the first day, at index 0, is never added. There is no crash and no message, only a total that is 4200 short.

Bounds go wrong in nested loops too. In this grid of cinema seats a 1 means taken, so four seats are taken:

Main.java
1public class Main {
2 public static void main(String[] args) {
3 int[][] seats = {
4 {1, 0, 1},
5 {1, 1, 0}
6 };
7 int taken = 0;
8 for (int r = 0; r < seats.length; r++) {
9 for (int c = 0; c < seats.length; c++) {
10 taken = taken + seats[r][c];
11 }
12 }
13 System.out.println("Seats taken: " + taken);
14 }
15}
Prints
›Seats taken: 3

It prints Seats taken: 3. The inner loop stops at seats.length, the number of rows, which is 2, so column 2 is never checked. As two-dimensional arrays showed, the number of columns in a row is seats[r].length.

Check both ends of every loop
For a loop over an array, work out by hand the first and the last value the index takes. The first should be 0 and the last should be length - 1, with the loop stopping right after it.

One equals sign where you meant two

If and else warned that = stores a value and == compares two. With an int, Java catches the slip for you, because an if needs a boolean:

Terminal
read along
1$ javac Main.java
2Main.java:4: error: incompatible types: int cannot be converted to boolean
3 if (lives = 0) {
4 ^
51 error

lives = 0 stores 0 in lives, and the whole expression has the value 0, an int. An int is not a yes-or-no answer, so javac refuses. With a boolean variable the same slip compiles, and the program quietly does the wrong thing:

Main.java
1public class Main {
2 public static void main(String[] args) {
3 boolean gameOver = false;
4 if (gameOver = true) {
5 System.out.println("Game over");
6 } else {
7 System.out.println("Keep playing");
8 }
9 System.out.println("gameOver is now " + gameOver);
10 }
11}
Prints
›Game over
›gameOver is now true

It prints Game over and then gameOver is now true. gameOver = true stores true and the if then tests that value, so the first branch always runs and the variable has changed as well. Write ==, or simply if (gameOver), since a boolean already is a yes-or-no answer.

Whole-number division

The average of 72, 85 and 90 is a little over 82.33. This program gets something else:

Main.java
1public class Main {
2 public static void main(String[] args) {
3 int[] scores = {72, 85, 90};
4 int total = 0;
5 for (int i = 0; i < scores.length; i++) {
6 total = total + scores[i];
7 }
8 double average = total / scores.length;
9 System.out.println("Average: " + average);
10 }
11}
Prints
›Average: 82.0

It prints Average: 82.0, and that .0 is the clue. The variable average is a double, but Java works out the right-hand side first, and total / scores.length is an int divided by an int, which drops the fraction, as operators and casting showed. The result, 82, becomes 82.0 only afterwards, when it is too late. Cast before dividing: (double) total / scores.length gives 82.33333333333333.

Forgetting to update a variable

A variable that should change inside a loop, but never does, is easy to miss because the code around it looks finished. Savings start at 100 and grow by 20 a week until they reach 150. Predict the output, then run it.

Predict the outputMain.java
1public class Main {
2 public static void main(String[] args) {
3 int savings = 100;
4 int weeks = 0;
5 while (savings < 150) {
6 savings = savings + 20;
7 }
8 System.out.println("Reached " + savings + " after " + weeks + " weeks");
9 }
10}

What does this print? Work through the loop on paper, writing down savings and weeks after each pass.

Ways to find a bug#

Compile errors and crashes point at a line. A logic error does not, so you need ways to close in on it. Programmers use the methods below every day, and you can use all of them already.

Explain it to a rubber duck

Read your code aloud, one line at a time, and explain what each line does, to a friend, a pet, or a rubber duck on your desk. It sounds silly, and it works more often than you would think. To explain a line you have to say what it really does, and that is when you hear yourself say “and then i starts at 1, which skips... oh.” This is called rubber duck debugging.

Predict, then check

Before you run a program, write down what you expect it to print, or what a variable should hold at some point. Then run it and compare. The first place where Java disagrees with you is either the bug or a gap in your understanding, and both are worth finding. Every Predict box in these notes is practice for this habit.

Labelled print statements

The quickest way to see inside a running program is to print what it holds. Add a println inside the loop, run the program, and read the values. Give every debug print a label, so you know which print produced each line and which variable each number belongs to. Here is the step counter again, with one labelled print at the top of the loop:

Main.java
1public class Main {
2 public static void main(String[] args) {
3 int[] steps = {4200, 6100, 3900, 8000};
4 int total = 0;
5 for (int i = 1; i < steps.length; i++) {
6 System.out.println("DEBUG i=" + i + ", steps[i]=" + steps[i] + ", total=" + total);
7 total = total + steps[i];
8 }
9 System.out.println("Steps this week: " + total);
10 }
11}
Prints
›DEBUG i=1, steps[i]=6100, total=0
›DEBUG i=2, steps[i]=3900, total=6100
›DEBUG i=3, steps[i]=8000, total=10000
›Steps this week: 18000

The first debug line already says i=1. The loop never visited index 0, so the 4200 was never added, and one run has found the bug.

Habits for debug prints
Start every debug line with the same word, such as DEBUG, so the lines are easy to spot and easy to remove. Print each name next to its value: total=6100 tells you far more than a bare 6100. When the bug is fixed, delete the debug prints.
Put the lines in orderMain.java
Put the lines in order so the program prints a labelled debug line after each lap time is added, and then the race time.

Tap a line, then tap where it should go. Or use the arrows.

The visualizer is a debugger

Every Run button in these notes opens a small step-by-step debugger. Press Run, pause, and use the arrows to move one step at a time. The line about to run is lit in the code, and the trace panel shows each variable in the call stack and each array in memory as it is at that step. Two features help with bugs in particular:

  • After a crash, the run ends on the error step. Step backwards from there to see the values that led to it. Most debuggers in editors can only move forwards.
  • The slider jumps to any moment, so you can find the first step where a value goes wrong and look at the line that did it.

Try it on the first step counter in this lesson: step through it and find the moment total first differs from your own running total.

Cut the search in half

When a long program goes wrong and you have no idea where, stop reading it line by line and halve the search instead. Pick a point near the middle and check whether the values are still right there, with a labelled print or a breakpoint. If they are already wrong, the bug is in the first half; if they are right, it is in the second. Then halve that half. A 64-line program needs only about six checks, because 64 halves to 32, 16, 8, 4, 2 and finally 1.

The same trick works on a compile error you cannot make sense of. Turn half the code into comments with //, which javac skips, and compile again. If the error disappears, it was in the half you hid. Bring lines back a few at a time until it returns. Comment out whole statements and whole blocks, or you will create new errors of your own.

Halving has a name
Cutting the search in half again and again is called binary search, and it is one of the fastest ways to find a value in a sorted list. You will write it yourself in searching and sorting.

The debugger in your editor#

Editors such as IntelliJ IDEA and VS Code come with a debugger: a tool that runs your program but lets you pause it, look at every variable, and go on one line at a time. It is the visualizer's idea, applied to your own programs on your own machine. The names of the controls are nearly the same in both editors:

ControlWhat it does
BreakpointA marker you set by clicking beside a line number. The program pauses just before that line runs.
DebugStarts the program under the debugger. Start it with Run instead and the breakpoints are ignored.
ResumeRuns on until the next breakpoint, or to the end. VS Code calls it Continue.
Step overRuns the current line completely, including any method it calls, and pauses on the next line that runs.
Step intoWhen the current line calls one of your own methods, goes inside and pauses on its first line.
Step outFinishes the current method and pauses back in the method that called it.
VariablesLists the variables of the method you are paused in, with their current values.
WatchesExpressions you add yourself, such as total or prices[i], worked out again at every pause.

Try them on this pretend debugger. A breakpoint is already set on line 6; press Debug, then use the buttons. withTax is a method of our own: main calls it with a price, and it hands back the price plus 10 percent tax. You will write methods like it in methods; here it is something to step into.

A pretend debuggernot running
1public class Main {
2 public static void main(String[] args) {
int[] prices = {20, 5, 12};
int total = 0;
for (int i = 0; i < prices.length; i++) {
total = total + withTax(prices[i]);
7 }
System.out.println("Total: " + total);
9 }
10
11 static int withTax(int price) {
int tax = price / 10;
return price + tax;
14 }
15}

Not running yet. The dot by line 6 is a breakpoint: click any line number to set or clear one, then press Debug.

Frames

not running

Variables

nothing to show

Watches

totalnot running
prices[i]not running

Console

nothing printed yet

Some things to try:

  • On line 6, press Step over and the whole call runs in one go. Press Step into instead and you land inside withTax, with a second method listed under Frames.
  • Inside withTax, the watches report no variable total or prices, because those belong to main. Click main under Frames and they come back.
  • From inside withTax, Step out finishes the method and lands back on line 6.
  • Clear every breakpoint and press Debug. With nowhere to pause, the program runs straight to the end.
Where to put the first breakpoint
Put it on the first line you suspect, or at the top of the loop that builds the wrong value. Then step over one line at a time, and before each press, say what you expect the variables to become. The first time a value differs from what you said, the line you just stepped over is the one to look at.
Quick check
You are paused on line 6 of the pretend debugger, total = total + withTax(prices[i]);, and you want to watch withTax work out the tax. Which button do you press?

A calm checklist#

When something goes wrong, the worst move is to start changing things at random. Each random change can add a new bug, and you learn nothing about the old one. Work down this list instead, and stop when you have found the problem.

  1. 1Read the whole message, slowly. For a compile error, find the file, the line and the caret. For a crash, read the exception name, the message and the first at line in your own file. For wrong output, write down what you expected next to what you got.
  2. 2With compile errors, fix the first one only, then compile again.
  3. 3Make the problem happen again on purpose, with the smallest input that shows it.
  4. 4Read the suspect lines aloud and say what each one does.
  5. 5Look inside: add labelled prints, or step through in the visualizer or a debugger, until you find the first line where a value stops matching your hand trace.
  6. 6Change one thing, then run again. If the change did not help, undo it before you try the next idea.
  7. 7Once it works, try the edges: an empty array, a single item, a zero, the last index. Then remove your debug prints.
  8. 8If you are still stuck after twenty minutes, take a short break, or explain the problem to someone else.
Key idea

Debugging is a search. Every check rules out places where the bug cannot be, until one line is left.

Now use the checklist on a program with three bugs in it.

ExerciseScores.java
This program should print the average and the top score of five exam scores. It has three bugs: one crash and two logic errors. Run the tests, read what comes back, fix one thing at a time, and run again until it prints Average: 77.4 and Top score: 92.

Try it

Answer each one in your head or on paper first, then open the answer.

1
Name the three kinds of problem, and say when each one shows up.
2
javac says cannot find symbol, and the next line says symbol: variable totl. Give two different mistakes that could cause this.
3
Without running it, say which exception this program throws and on which line. Then run it to check. Why is starting the loop at 1 not a bug here?
4
With int total = 247; and int count = 3;, why does double average = total / count; give 82.0, and how do you fix it?
5
In one sentence each, what do step over, step into and step out do?
6
Read this message piece by piece: Cannot invoke "String.length()" because "last" is null. What failed, and why?

Practice

Work through these on your own. Each one builds on this page.

1
Break a working program on purpose in five ways, one at a time: delete a semicolon, misspell a variable, put a String into an int, and delete a closing brace. Then copy the grade example, fix it with a plain else, and change that else back to else if (score < 50). Compile each version with javac and write the first line of each message in your notes next to the mistake that caused it.
2
Write a program that adds up the even numbers in an array, then slip one logic bug into it on purpose, such as a wrong loop bound. Swap programs with a friend, or put yours away until tomorrow, and find the bug using only labelled prints.
3
Install an editor that has a debugger, such as IntelliJ IDEA, or VS Code with the Extension Pack for Java. Type in the step counter from this lesson, set a breakpoint on the line inside the loop, start it with Debug, and step over until you see the moment total goes wrong. Write down what the Variables panel showed at that moment.
4
Take the savings program and fix the weeks bug so that it prints Reached 160 after 3 weeks. Then break it differently: delete the line savings = savings + 20;. Predict what will happen before you run it, run it in the playground, and explain what you see.
5
From memory, write the calm checklist as a numbered list on a card, in your own words, and keep it next to your keyboard for the rest of the course.

Further reading#

Spotted a mistake, or a part that did not make sense? Report it by email