JAVA / EXCEPTIONS
Stack traces and debugging a live failure
Read a Java stack trace top-down, locate the first frame you own, follow the Caused by chain to the root cause, and turn that into a reproducible fix.
What you will learn
- Frame 0 is where the exception was constructed; every line below it is a caller.
- Skip JDK frames and fix at the topmost frame in a class you wrote.
- The bottom-most Caused by is the root cause; '... N more' means repeated frames.
- Use getStackTrace() to filter frames as data instead of scanning printed text.
Understanding Stack traces and debugging a live failure
A Throwable captures the call stack inside its constructor, not at the point where it is thrown or caught: the constructor calls fillInStackTrace(), which copies the frames of the current thread into the object. The printed form starts with the fully qualified type and the detail message, then one at line per frame, innermost first, so at Main.loadRow(Main.java:5) means the exception was created while line 5 of Main.loadRow was executing, and the line under it is whoever called loadRow. The last frame is the entry point of the thread that failed, which is how you tell whether main died or a pool thread did.
Frames tell you the path; the message tells you the value. In a live failure the top frames are often JDK or framework code such as java.util.Objects.checkIndex or ArrayList.get, and that code is usually behaving correctly, having detected input your code produced. So read downwards until the first frame in a class you own: that line, plus the concrete value in the message like Index 4 out of bounds for length 2, is normally enough to name the bug without attaching a debugger.
Wrapping produces a chain, and printStackTrace renders it outermost first with Caused by: sections that end in a line like ... 27 more, meaning the remaining frames are identical to those already printed above rather than lost. The outer exception is only a label added by the layer that noticed the problem, so the bottom-most Caused by block is where the real failure lives; start there and read upward if you need to know how the call arrived. In code the same structure is getCause() until it returns null, and getStackTrace() hands you the frames as StackTraceElement objects you can filter.
public class Main {
static int[] budget = new int[3];
static void loadRow(int row) {
budget[row] = 100;
}
static void importAll() {
for (int row = 0; row <= 3; row++) {
loadRow(row);
}
}
public static void main(String[] args) {
try {
importAll();
} catch (ArrayIndexOutOfBoundsException e) {
StackTraceElement[] frames = e.getStackTrace();
System.out.println("class: " + e.getClass().getName());
System.out.println("message: " + e.getMessage());
System.out.println("thrown in: " + frames[0].getMethodName() + " (line " + frames[0].getLineNumber() + ")");
System.out.println("called by: " + frames[1].getMethodName() + " (line " + frames[1].getLineNumber() + ")");
System.out.println("called by: " + frames[2].getMethodName() + " (line " + frames[2].getLineNumber() + ")");
}
}
}A stack trace is a snapshot of the thread's call chain taken when the exception object was constructed, so it names both the failure site and the path that reached it.
Worked examples
Walking a cause chain to the root failure
Shows that the exception you catch is only the outer label and that getCause() leads to the original problem.
public class Main {
static int readTimeout(String raw) {
return Integer.parseInt(raw);
}
static void startService(String raw) {
try {
readTimeout(raw);
} catch (NumberFormatException cause) {
throw new IllegalStateException("bad timeout setting: " + raw, cause);
}
}
public static void main(String[] args) {
try {
startService("eight");
} catch (IllegalStateException e) {
Throwable t = e;
int level = 0;
while (t != null) {
System.out.println(level + ": " + t.getClass().getSimpleName() + " / " + t.getMessage());
t = t.getCause();
level++;
}
}
}
}Example explained
Line 1new IllegalStateException(message, cause) parks the original exception, with its own frames, in the cause slot.
Line 2getCause() returns null past the end of the chain, which both ends the loop and identifies the root cause.
Line 3In printed form this chain appears as the outer trace followed by a Caused by: block for level 1.
Line 4Level 1 is where debugging starts, because only it names the offending input, "eight".
Finding the topmost frame you actually own
Filters the trace down to frames declared in your own class, which is where a fix can be applied.
import java.util.ArrayList;
import java.util.List;
public class Main {
static List<String> rows = new ArrayList<>();
static String rowAt(int index) {
return rows.get(index);
}
static void report(int index) {
System.out.println(rowAt(index).toUpperCase());
}
public static void main(String[] args) {
rows.add("north");
rows.add("south");
try {
report(4);
} catch (IndexOutOfBoundsException e) {
System.out.println(e.getMessage());
for (StackTraceElement f : e.getStackTrace()) {
if (f.getClassName().equals("Main")) {
System.out.println("my code: " + f.getMethodName() + " line " + f.getLineNumber());
}
}
}
}
}Example explained
Line 1The exception is built inside the JDK's bounds-check helpers called from ArrayList.get, so the real frame 0 is not printed here.
Line 2The filter on getClassName().equals("Main") is the mechanical version of scanning down to code you can change.
Line 3rowAt line 8 is the fix site because it forwards an unvalidated index; report line 12 and main line 19 explain how the value 4 arrived.
Line 4The number of JDK frames above rowAt varies between releases, which is exactly why you filter by class instead of counting frames.
Reading a trace without throwing anything
Uses a Throwable purely as a stack snapshot to answer the question 'who called this method?' during a live investigation.
public class Main {
static void invalidateCache(String reason) {
StackTraceElement caller = new Throwable().getStackTrace()[1];
System.out.println("invalidateCache(" + reason + ") from "
+ caller.getMethodName() + " line " + caller.getLineNumber());
}
static void onSave() {
invalidateCache("save");
}
static void onLogout() {
invalidateCache("logout");
}
public static void main(String[] args) {
onSave();
onLogout();
}
}Example explained
Line 1new Throwable() fills in the frames during construction, so the object is a usable stack snapshot even though it is never thrown.
Line 2Frame 0 is always the method that constructed the Throwable, which makes index 1 its direct caller.
Line 3The line numbers come from the LineNumberTable in the class file; code compiled with -g:none prints Unknown Source instead.
Line 4Capturing a trace walks the whole stack, so this belongs in a temporary investigation, not in a hot loop.
Important notes
printStackTrace() writes to System.err while your program prints to System.out, so on a shared console a trace can appear next to unrelated output and look like it belongs to another operation.
Detail messages are diagnostics, not API: JDK 8 printed only the index for ArrayIndexOutOfBoundsException, and JDK 15 turned on NullPointerException messages that name the null expression, so read the wording but never parse it in code.
Common mistakes
Reading only the first at line, concluding that ArrayList.get or Objects.checkIndex is broken, and never opening the line in their own class that passed the bad index, so the bug outlives the supposed fix.
Logging e.getMessage() instead of the exception object: a NullPointerException with no message produces the log line 'error: null' and every frame and line number is discarded.
Debugging the outer exception of a wrapped failure, spending an hour on "bad timeout setting" while the last Caused by already said For input string: "eight".
Try it yourself
Change, predict, then run
In a browser editor, build a three-deep call chain whose innermost method calls Integer.parseInt("eight"), catch the NumberFormatException one level up and rethrow it as an IllegalStateException that carries the cause. In main, walk getCause() and print each exception's simple name, message, and getStackTrace()[0].getMethodName(), then check that the root cause's frame 0 sits inside java.lang rather than in your class.
Open the Java workspaceCheck your understanding
A project declares static final IllegalStateException CLOSED = new IllegalStateException("closed"); and several methods do throw CLOSED;. Why are the resulting traces useless for locating the failure?
- Rethrowing the same object clears its frames, so the trace prints with no at lines at all.
- The frames are overwritten on each throw, so the trace only ever shows whichever method threw last.
- The frames were captured when the static field was initialised, so every trace points into the class initializer instead of the method that threw.
- IllegalStateException records frames only when it is constructed with a cause.
Show answer
fillInStackTrace() runs inside the Throwable constructor, so the only snapshot this object will ever hold is of the static initializer that created it; throw never touches the frames. Option 1 is tempting because traces normally do point at a throw site, but that is only because the usual pattern is throw new ..., which constructs the exception exactly where the failure happens.