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
javacbefore 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
^symboljavacprints 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
tryandcatchso 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.
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.
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 }.
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
}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.
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.
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;
}
}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.
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.
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
Make and read three compiler errors
Take Hello World and, one at a time, (1) delete the semicolon, (2) change
printlntoprintn, (3) putint x = "one";insidemain. Before compiling each, predict the message. Then compile and compare with what you predicted. - 2
Read a real stack trace
Save the
averageprogram from the walkthrough and run it withjava Main.java. Find the line in the trace that namesaverage, go to that line in your file, and changecountso the division works (for example passscores.length).terminal$ java Main.java── expected output ──Exception in thread "main" java.lang.ArithmeticException: / by zeroat Main.average(Main.java:12)at Main.main(Main.java:4) - 3
See the cascade shrink
Remove the quotes from a
printlntext, 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
Each try/catch catches the exception so the program keeps going (Topic 7.2). getClass().getSimpleName() is the exception type, getMessage() its message.
Expected output
ArithmeticException: / by zero
ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
NumberFormatException: For input string: "abc"
The program survived all three.getStackTrace() returns the same frames the JVM prints. The top frame is where the exception was thrown.
Expected output
Caught: java.lang.ArithmeticException: / by zero
at average, line 19
at main, line 5getCause() returns the wrapped exception. Walking the chain to the end finds the root cause.
Expected output
Top level: Bad settings line: port=abc
Root cause: java.lang.NumberFormatException: For input string: "abc"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].
Break #2
A decimal into a whole-number box
Write int half = 7.5;.
Break #3
A method that calls itself forever
Run the countDown program from the code examples, which has no stopping condition.
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.
-Xmaxerrsand-Xmaxwarnschange the default limit of 100 each;-Xlint:allturns on extra warnings worth reading. - ▸
The class file stores a
LineNumberTable(on by default) mapping bytecode offsets to source lines, and aLocalVariableTableonly with-gor-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 whengetMessage()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 (
fillInStackTracewalks the stack). The JIT can also throw pre-allocated exceptions without traces for very hot implicit exceptions (-XX:-OmitStackTraceInFastThrowdisables 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 beforeStackOverflowErrordepends on frame size, so it varies between JIT-compiled and interpreted code.
Remember this
- 1
There are three kinds of problems. Compile-time errors: the code breaks Java's rules,
javacrefuses 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
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 assymbol:andlocation:, and finally a count like1 error. Read the line number and the message first, then look at the caret. - 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,
javacchecks 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
A runtime crash prints
Exception in thread "main", then the exception type (such asjava.lang.ArithmeticException), a colon and a message (such as/ by zero). Below that is the stack trace: lines starting withat, each one a method that was running, newest at the top.at Main.average(Main.java:12)means "in methodaverageof classMain, fileMain.java, line 12". - 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, aCaused by:section follows, and the lastCaused byis usually the root cause.... 1 morejust means the remaining lines are the same as the trace above. - 6
Since Java 14,
NullPointerExceptionmessages say exactly what wasnull: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
What is the difference between a compile-time error, a runtime error and a logic error? Give an example of each.
Walk through every part of this message: Main.java:4: error: cannot find symbol with symbol: variable cuont and location: class Main.
How do you read a stack trace to find the bug quickly?
Why can fixing one compiler error make new errors appear?
What changed about NullPointerException messages in Java 14, and why does <local1> sometimes appear?
Practice
Write a program that catches the exception from Integer.parseInt("12a") and prints the exception's simple class name and message.
Write a program that safely prints the last element of {4, 8, 15} without hard-coding the index.
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
-gmakes 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
javacerror: 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.