Command Palette

Search for a command to run...

PHASE 0Beginner ~32 min· topic 7 of 8

Topic 0.7

Reading Compiler Errors and Stack Traces

In one line

Java reports problems in two places: the compiler (javac) rejects code that breaks the rules, giving the file, line, a message and a caret ^ under the spot; and the JVM reports problems while running as an exception with a stack trace, a list of the method calls that led to the crash. Reading both calmly, top line first, is the most useful debugging skill you can learn.

Think of it like this

A GPS that says "wrong turn at Park Street, 200 metres back". It tells you where you went wrong and what went wrong. Java's error messages are the same: Main.java:4 is the street address, the message is what went wrong, and the ^ is the finger pointing at the exact spot. Programmers aren't people who never see errors; they are people who read them carefully.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Compile-time error
A rule broken in the source code, found by javac before the program runs. No class file is produced.
Runtime error
A problem that happens while the program runs, such as dividing by zero. Java reports it by throwing an exception.
Exception
An object the JVM creates and "throws" when something goes wrong while running. If nothing catches it, the program stops and prints it.
Stack trace
The list of method calls that were active when an exception happened, newest first, each with a file name and line number.
Caret
The ^ symbol javac prints under a line to point at the spot where it found the problem.
Symbol
Any name in your code: a variable, method or class. "cannot find symbol" means a name the compiler doesn't know.
Root cause
The original problem behind a chain of errors. In a stack trace it is usually the last Caused by:.
Warning
A message about something suspicious that is still legal. The program compiles anyway.
Catch
Handle an exception in your code with try and catch so the program doesn't crash (Topic 7.2).

Step by step

01Anatomy of a compiler error

Here the variable is called count, but line 4 uses cuont. Read the message in parts. Main.java:4 is the file and line. error: cannot find symbol is the problem. The copied line and the caret show where. symbol: variable cuont names the unknown thing, and location: class Main says where Java looked for it.

"cannot find symbol" nearly always means a typo, a wrong capital letter, a variable used outside the block where it was declared (Topic 3.6), or a missing import.

terminal
$ javac Main.java
── expected output ──
Main.java:4: error: cannot find symbol
System.out.println(cuont);
^
symbol: variable cuont
location: class Main
1 error

02The caret points where Java noticed, not always where you erred

For a missing semicolon, the caret sits just after the end of the line, because that is where the compiler expected the ;. For a missing closing brace, the error is reported at the end of the file, which can be far from the brace you forgot.

So if the marked line looks fine, look at the line just above it. That is where the real mistake usually is.

terminal
$ javac Main.java
── expected output ──
Main.java:3: error: ';' expected
System.out.println("Hello")
^
1 error

03Common compiler messages and what they mean

';' expected: a statement is missing its semicolon. cannot find symbol: unknown name (typo, capital letter, scope, import). incompatible types: String cannot be converted to int: you put a value of one type where another is required. possible lossy conversion from double to int: a decimal can't go into a whole-number box without an explicit cast (Topic 1.6).

variable total might not have been initialized: you read a local variable before giving it a value. missing return statement: a method promises to return a value but some path doesn't. unreachable statement: code after a return or an endless loop can never run. class Main is public, should be declared in a file named Main.java: file and class names differ. reached end of file while parsing: a missing }.

Main.javawhole filejava
public class Main {
    public static void main(String[] args) {
        int total;
        System.out.println(total);      // read before a value was set
        return;
        System.out.println("never");    // can never run
    }

    static int pick(int n) {
        if (n > 0) {
            return 1;
        }
    }                                   // no return when n <= 0
}
terminal
$ javac Main.java
── expected output ──
Main.java:6: error: unreachable statement
System.out.println("never");
^
Main.java:13: error: missing return statement
}
^
Main.java:4: error: variable total might not have been initialized
System.out.println(total);
^
3 errors

04javac checks in stages

The compiler works in passes: first it reads the grammar (parsing), then it checks names and types, then it analyses the flow of the program (does every path return? is every variable set before use?). If an early pass finds errors, later passes may not run at all.

In the file above, adding the line int half = 7.5; makes javac report only the lossy-conversion error, because type checking failed before flow analysis started. Fix it, compile again, and the three flow errors appear. Don't panic when the error count goes up after a fix.

javac checks in stagesdiagram
Rendering diagram…

05Anatomy of a stack trace

This program compiles fine, then divides by zero while running. The JVM throws an ArithmeticException. Nothing catches it, so the main thread dies and the JVM prints the exception and the stack trace, then exits with status code 1.

Read it top down. Line 1: what went wrong (ArithmeticException: / by zero). Line 2: where it happened (average, line 12). Line 3: who called that (main, line 4). The method that crashed is on top, and the start of the program is at the bottom.

Main.javawhole filejava
public class Main {
    public static void main(String[] args) {
        int[] scores = {90, 75, 60};
        System.out.println("Average: " + average(scores, 0));
    }

    static int average(int[] values, int count) {
        int total = 0;
        for (int v : values) {
            total += v;
        }
        return total / count;
    }
}
terminal
$ java Main
── expected output ──
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.average(Main.java:12)
at Main.main(Main.java:4)
Anatomy of a stack tracediagram
Rendering diagram…

06Caused by: exceptions inside exceptions

Code often catches a low-level exception and throws a clearer one, keeping the original as its cause. Then the trace has two parts. The first says what the program was doing (Bad settings line: port=abc). The Caused by: part says what actually broke (NumberFormatException: For input string: "abc").

Notice the JDK frames (java.base/java.lang.Integer.parseInt) above your own frame Main.loadSettings(Main.java:8). The JDK is fine; your code gave it text that isn't a number. ... 1 more means the rest of this trace matches the frames already shown above.

terminal
$ java Main
── expected output ──
Exception in thread "main" java.lang.IllegalStateException: Bad settings line: port=abc
at Main.loadSettings(Main.java:10)
at Main.main(Main.java:3)
Caused by: java.lang.NumberFormatException: For input string: "abc"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at java.base/java.lang.Integer.parseInt(Integer.java:778)
at Main.loadSettings(Main.java:8)
... 1 more

07Helpful NullPointerException messages

null means "no object here". Calling a method on null throws a NullPointerException. Before Java 14 the message was empty, and on a line like a.b().c().d() you had to guess which part was null.

Since Java 14 the JVM explains it. Compiled with javac -g (which keeps local variable names), the message names the variable; without -g it says "<local1>" (local variable slot 1). Build tools such as Maven and Gradle compile with names by default.

terminal
$ javac -g Main.java
java Main
── expected output ──
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
at Main.main(Main.java:4)

08A calm routine for any error

1. Read the first message completely, out loud if it helps. 2. Go to the file and line. 3. Look at the caret, and at the line above. 4. For an exception, find the topmost line in your own code, and the last Caused by. 5. Change one thing, then compile or run again.

If the message is still unclear, copy the message part (not your variable names) into a search engine; thousands of people have hit the same one. Topic 7.8 covers how to write code that produces good error messages for others.

Try it yourself

  1. 1

    Make and read three compiler errors

    Take Hello World and, one at a time, (1) delete the semicolon, (2) change println to printn, (3) put int x = "one"; inside main. Before compiling each, predict the message. Then compile and compare with what you predicted.

  2. 2

    Read a real stack trace

    Save the average program from the walkthrough and run it with java Main.java. Find the line in the trace that names average, go to that line in your file, and change count so the division works (for example pass scores.length).

    terminal
    $ java Main.java
    ── expected output ──
    Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Main.average(Main.java:12)
    at Main.main(Main.java:4)
  3. 3

    See the cascade shrink

    Remove the quotes from a println text, as in Topic 0.1's break-it. Count the errors (five). Put back only the opening quote and closing quote, compile again: zero errors. One real mistake, five messages.

Code & diagrams

Catching common runtime exceptions New tab

Each try/catch catches the exception so the program keeps going (Topic 7.2). getClass().getSimpleName() is the exception type, getMessage() its message.

Sign in to run this example in your browser.

Expected output

ArithmeticException: / by zero
ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
NumberFormatException: For input string: "abc"
The program survived all three.
Printing a stack trace yourself New tab

getStackTrace() returns the same frames the JVM prints. The top frame is where the exception was thrown.

Sign in to run this example in your browser.

Expected output

Caught: java.lang.ArithmeticException: / by zero
  at average, line 19
  at main, line 5
Following the cause chain New tab

getCause() returns the wrapped exception. Walking the chain to the end finds the root cause.

Sign in to run this example in your browser.

Expected output

Top level: Bad settings line: port=abc
Root cause: java.lang.NumberFormatException: For input string: "abc"
Infinite recursion: StackOverflowErrorjava

Not runnable on purpose: it crashes. A method that calls itself forever fills the call stack. The trace repeats the same frame about a thousand times (the JVM prints at most 1024 by default).

public class Main {
    public static void main(String[] args) {
        countDown(3);
    }

    static void countDown(int n) {
        countDown(n - 1);   // no stopping condition
    }
}
// Exception in thread "main" java.lang.StackOverflowError
//     at Main.countDown(Main.java:7)
//     at Main.countDown(Main.java:7)
//     ...

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Read past the end of an array

With int[] scores = {90, 75, 60};, print scores[3].

terminal
$ java Main.java
── what you'll see ──
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
at Main.main(Main.java:4)

Break #2

A decimal into a whole-number box

Write int half = 7.5;.

terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: incompatible types: possible lossy conversion from double to int
int half = 7.5;
^
1 error

Break #3

A method that calls itself forever

Run the countDown program from the code examples, which has no stopping condition.

terminal
$ java Main.java
── what you'll see ──
Exception in thread "main" java.lang.StackOverflowError
at Main.countDown(Main.java:7)
at Main.countDown(Main.java:7)
at Main.countDown(Main.java:7)
...

Myth vs fact

Myth

Lots of errors means lots of mistakes.

Fact

Often one mistake causes many messages. Fix the first one and recompile; most of the rest usually disappear.

Myth

The error is always on the line the compiler names.

Fact

The compiler names the line where it noticed the problem. For missing semicolons, quotes or braces, the real mistake is often on the line before.

Myth

If the stack trace shows JDK classes, the bug is in Java.

Fact

The JDK frames show where the problem surfaced. The bug is almost always in what your code passed in. Look for the first frame from your own classes.

Myth

Catching every exception makes a program robust.

Fact

Catching and ignoring exceptions hides bugs. Catch only what you can handle sensibly; let the rest surface with a clear stack trace (Topic 7.8).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    javac's pipeline is parse, enter, attribute (types and names), flow (definite assignment, reachability, exceptions), desugar and generate. Errors in an earlier phase suppress later phases for those classes, which is why fixing a type error can reveal flow errors. -Xmaxerrs and -Xmaxwarns change the default limit of 100 each; -Xlint:all turns on extra warnings worth reading.

  • ▸

    The class file stores a LineNumberTable (on by default) mapping bytecode offsets to source lines, and a LocalVariableTable only with -g or -g:vars. Stack traces use the first; helpful NPE messages use the second for names, falling back to <localN>. Helpful NPEs (JEP 358) are computed lazily, only when getMessage() is called, so they cost nothing for NPEs that are caught and ignored.

  • ▸

    Building a stack trace is the expensive part of creating an exception (fillInStackTrace walks the stack). The JIT can also throw pre-allocated exceptions without traces for very hot implicit exceptions (-XX:-OmitStackTraceInFastThrow disables that), which is why a production log can show an NPE with no stack trace after it has happened many times.

  • ▸

    Default trace depth is 1024 frames (-XX:MaxJavaStackTraceDepth), which matters for deep recursion. Thread stack size is set with -Xss (often 512 KB to 1 MB by default on 64-bit platforms); recursion depth before StackOverflowError depends on frame size, so it varies between JIT-compiled and interpreted code.

Remember this

  1. 1

    There are three kinds of problems. Compile-time errors: the code breaks Java's rules, javac refuses to produce a class file, and nothing runs. Runtime errors: the code compiles, but something impossible happens while it runs (dividing by zero, using a missing array slot), and the JVM throws an exception. Logic errors: the program runs and finishes, but the answer is wrong, and no message tells you (Topic 0.1).

  2. 2

    A compiler error has a fixed shape: File.java:LINE: error: MESSAGE, then a copy of the line, then a caret ^ under the place where the compiler noticed the problem, sometimes extra lines such as symbol: and location:, and finally a count like 1 error. Read the line number and the message first, then look at the caret.

  3. 3

    Fix the first error first, then compile again. One mistake, such as a missing quote or brace, often confuses the compiler about everything after it, producing a cascade of errors that vanish once the first is fixed. Also, javac checks in stages: after you fix errors from one stage, errors from a later stage (like "missing return statement") can appear. That is progress, not new breakage.

  4. 4

    A runtime crash prints Exception in thread "main", then the exception type (such as java.lang.ArithmeticException), a colon and a message (such as / by zero). Below that is the stack trace: lines starting with at, each one a method that was running, newest at the top. at Main.average(Main.java:12) means "in method average of class Main, file Main.java, line 12".

  5. 5

    Read a stack trace from the top, and look for the first line that mentions your own code. Lines in java.base/... are inside the JDK; the bug is almost never there, but in what your code passed to it. When an exception wraps another, a Caused by: section follows, and the last Caused by is usually the root cause. ... 1 more just means the remaining lines are the same as the trace above.

  6. 6

    Since Java 14, NullPointerException messages say exactly what was null: Cannot invoke "String.length()" because "name" is null. These helpful NullPointerException messages are on by default since Java 15. Local variable names appear only if the class was compiled with -g; otherwise you see "<local1>", which still tells you which slot was null.

Explain it without notes

01

What is the difference between a compile-time error, a runtime error and a logic error? Give an example of each.

02

Walk through every part of this message: Main.java:4: error: cannot find symbol with symbol: variable cuont and location: class Main.

03

How do you read a stack trace to find the bug quickly?

04

Why can fixing one compiler error make new errors appear?

05

What changed about NullPointerException messages in Java 14, and why does <local1> sometimes appear?

Practice

01

Write a program that catches the exception from Integer.parseInt("12a") and prints the exception's simple class name and message.

02

Write a program that safely prints the last element of {4, 8, 15} without hard-coding the index.

03

This method doesn't compile: static int sign(int n) { if (n > 0) return 1; else if (n < 0) return -1; }. Explain the error and fix it, then print sign(-5), sign(0) and sign(9).

Trade-offs

  • ↔

    Java catches many mistakes at compile time (types, missing returns, uninitialised variables), which costs a little ceremony but saves runtime crashes. Languages that check later are quicker to write but show more errors in production.

  • ↔

    Catching exceptions keeps a program running, but swallowing them hides bugs. Catch where you can do something useful (retry, use a default, report clearly) and let the rest propagate.

  • ↔

    Compiling with -g makes NPE messages and debuggers more helpful at the cost of slightly larger class files. Build tools do it by default, and it is almost always worth it.

Done when you can

  • Done when you can name the parts of a javac error: file, line, message, caret, details, count.

  • Done when you fix errors from the first one down and expect new ones after a fix.

  • Done when you can explain the common messages: ';' expected, cannot find symbol, incompatible types, missing return statement.

  • Done when you can read a stack trace top-down and find the first frame in your own code.

  • Done when you can find the root cause in a Caused by: chain.

  • Done when you can read a helpful NullPointerException message.