RizTech Academy logo
RizTech Academy
Exceptions and Error HandlingLesson 2 of 625 min

Reading a stack trace, and the debugger

A stack trace is the single most useful thing Java gives you, and beginners scroll past it because it looks like forty lines of noise.

It is not noise. It tells you what went wrong, where, and how the program got there. Learning to read one in five seconds rather than five minutes is probably the highest-value skill in this module.

A real one

static void loadReport() { parseRows(List.of("Priya,26", "Arjun,eighteen")); }
static void parseRows(List<String> rows) { for (String row : rows) parseRow(row); }
static void parseRow(String row) { Integer.parseInt(row.split(",")[1]); }
java.lang.NumberFormatException: For input string: "eighteen"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at Trace.parseRow(Trace.java:33)
	at Trace.parseRows(Trace.java:27)
	at Trace.loadReport(Trace.java:22)
	at Trace.main(Trace.java:7)

How to read it, in order

1. The first line is the answer. Type and message:

java.lang.NumberFormatException: For input string: "eighteen"

You now know what failed and with what data. Often that is enough — the message names the offending value, which is why the last module kept insisting on messages that include it.

2. The frames are innermost first. Line two is where the exception was created; the bottom is where the program started. Read top to bottom to go outwards, bottom to top to follow the program's path.

3. Find the first line that is your code. The top three frames are java.base — the standard library. They are almost never the bug.

	at Trace.parseRow(Trace.java:33)

That is the line to open. Trace.java, line 33. Your code, your mistake.

4. Read upwards from there for the context. parseRow was called by parseRows, called by loadReport, called by main. That chain tells you which row you were on and what the program was doing.

The whole procedure is: read the first line, then find the first frame that belongs to you. Two steps. Everything else is detail you may or may not need.

Anatomy of a frame

	at com.riztech.tiffin.ReportService.parseRow(ReportService.java:33)
	   └── package ────────────────┘ └class──────┘ └method┘ └file──────┘ └line

java.base/ before a frame means it came from a JDK module — library code. Your own frames have no module prefix in an ordinary application.

Caused by, and why it is the interesting part

Wrap the failure and the trace changes shape:

java.lang.IllegalStateException: report row could not be parsed
	at Trace.wrapped(Trace.java:40)
	at Trace.main(Trace.java:15)
Caused by: java.lang.NumberFormatException: For input string: "eighteen"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at Trace.parseRow(Trace.java:33)
	at Trace.parseRows(Trace.java:27)
	at Trace.loadReport(Trace.java:22)
	at Trace.wrapped(Trace.java:38)
	... 1 more

Scroll to the bottom. The top exception is the one nearest your caller; the last Caused by is the one that actually went wrong. In a Spring application you will routinely see four or five, and the real problem is always at the bottom.

... 1 more means the remaining frames are identical to the ones already printed above. It is a space saver, not missing information.

Wrapping without a cause destroys all of this. The Caused by section is exactly what you lose, and it is the section with the answer in it.

Traces that need different treatment

StackOverflowError — hundreds of repeating frames. Look for the shortest repeating cycle at the top; that is your infinite recursion. Usually a method calling itself, or toString() calling something that calls toString().

OutOfMemoryError: Java heap space — the trace shows where memory ran out, which is rarely where it was leaked. Do not chase the frame; look for what is accumulating.

NoClassDefFoundError / ClassNotFoundException — nothing to do with your logic. A dependency is missing from the classpath at runtime. Module 10's territory.

A trace with no frames of yours in it at all — usually a framework reflection layer. Read the message and the Caused by.

An exception with a null message — the constructor took no message. NullPointerException used to be the worst offender, which is what the next section is about.

Helpful NullPointerException messages

Before Java 14, an NPE on a chained expression told you the line and nothing else, and if the line had four dereferences on it you got to guess which. Java 14 added helpful messages, on by default since 15:

Map<String, List<String>> routes = new HashMap<>();
int n = routes.get("Wagholi").size();
Cannot invoke "java.util.List.size()" because the return value of "java.util.Map.get(Object)" is null

It names the method you tried to call and what was null. Two more:

Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null

Cannot invoke "Exc$Customer.name()" because the return value of "Exc$Order.customer()" is null

The first is the unboxing trap: int n = map.get(key) on a missing key. The message says intValue(), which is the unboxing call the compiler inserted — if you see intValue() in an NPE and you never wrote it, that is what happened.

Read the whole message. It usually contains the entire diagnosis, and it is one of the best things to arrive in Java in a decade.

Using the debugger instead

A stack trace tells you where. A debugger tells you with what values.

In IntelliJ:

  1. Click the gutter beside a line to set a breakpoint.
  2. Run with the debug button rather than run.
  3. When it stops: F8 steps over, F7 steps into, F9 continues.
  4. The Variables panel shows everything in scope. Hover over any expression.
  5. Alt+F8 evaluates an arbitrary expression at that point.

Two features worth knowing on day one:

Conditional breakpoints. Right-click a breakpoint and give it a condition — row.contains("eighteen") — so it stops only on the interesting iteration rather than all four thousand.

Exception breakpoints. Run → View Breakpoints → + → Java Exception Breakpoint → NumberFormatException. The debugger now stops the moment one is thrown, anywhere, with every local variable still alive. For a failure you can reproduce but cannot locate, this is the fastest tool there is.

Print statements are not shameful, and for a loop of ten thousand iterations they are often faster than stepping. But learn the debugger — an hour spent on it pays back within a week.

Check your work

What are the two steps for reading a trace? Read the first line for the type and message. Then find the first frame that is your code, not java.base.

Which end of the frame list is the origin of the exception? The top. The bottom is where the program started.

In a trace with several Caused by sections, which matters most? The last one. That is the original failure; everything above it is wrapping.

What does ... 1 more mean? The remaining frames are identical to ones already printed. Nothing is missing.

You see intValue() in a NullPointerException but never wrote it. What happened? Unboxing — something like int n = map.get(key) where the key was absent, so null.intValue() was called by compiler-inserted code.

What should you do about hundreds of repeating frames? Look for the shortest repeating cycle at the top; that is the recursion that does not terminate.

What does a conditional breakpoint give you? It stops only when the condition holds, so you can reach the one interesting iteration out of thousands.

Practice 2, finding the line. In the trace shown, the first non-java.base frame is Trace.parseRow(Trace.java:33). That is the line to open. parseRows and loadReport above it tell you the context — you were iterating rows — but the bug is at line 33.

Practice 4, the lost cause. Removing the cause from new IllegalStateException("report row could not be parsed") deletes the entire Caused by block. What survives says a row could not be parsed; what is gone is which value failed ("eighteen"), which line it failed on (Trace.java:33), and what kind of failure it was. Every part you would use to fix it.

Practice

  1. Generate a trace on purpose. Write the three-level parse chain and let it fail. Print it with e.printStackTrace().

  2. Find your line in five seconds. Cover the message, look only at the frames, and name the file and line to open. Then check.

  3. Make a wrapped trace. Catch the NumberFormatException and rethrow an IllegalStateException with the cause. Identify the real failure by reading only the bottom of the output.

  4. Delete the cause. Rethrow without the second constructor argument and compare the two traces. Write down each piece of information you lost.

  5. Collect three helpful NPE messages. One from a chained method call, one from unboxing a missing map value, and one from a record accessor chain. Read each aloud as a sentence — they are written to be read that way.

  6. Harder — use the debugger properly. Take the parse loop over four rows, set a conditional breakpoint that only triggers on the bad row, and inspect parts in the Variables panel. Then remove it, add a NumberFormatException exception breakpoint, and run again. Write one sentence on which you would reach for in a codebase you do not know.

Next: checked versus unchecked exceptions, and the twenty-year argument about them.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship