Debugging is a skill, not a talent, and most of it is one habit: read the error before you do anything else. Java's errors are unusually informative, and beginners routinely skip past them to start guessing.
Reading a stack trace
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.length()" because "name" is null
at com.lovemesomecoding.OrderService.validate(OrderService.java:42)
at com.lovemesomecoding.OrderService.placeOrder(OrderService.java:31)
at com.lovemesomecoding.App.main(App.java:12)
Four pieces of information, in order of usefulness:
- The message. Since Java 14, a
NullPointerExceptionnames the thing that was null and what you tried to do with it — "becausenameis null". That sentence usually is the answer. - The top frame is where it was thrown:
OrderService.java:42. - Reading down tells you how you got there.
maincalledplaceOrder, which calledvalidate. - The first line that is your code is where to start. A trace that begins with twenty framework frames is common; scroll to your package name.
The top frame is where the problem surfaced, which is not always where it was caused. A null at line 42 often means something failed to set it at line 20.
"Caused by" — read the bottom one first
Exception in thread "main" com.lovemesomecoding.DataAccessException: load failed
at com.lovemesomecoding.Repo.load(Repo.java:55)
...
Caused by: java.sql.SQLException: Connection refused
at org.postgresql.Driver.connect(Driver.java:120)
... 12 more
Each Caused by is the exception that triggered the one above it. The last
Caused by in the trace is the original failure — and it is nearly always the
one that tells you what actually went wrong. Here, the real problem is not "load failed"; it is that
the database is not accepting connections.
This is why Exception Handling insists on passing the cause when you wrap an exception. Drop it and this whole section disappears from the trace.
The errors you will meet most
| Exception | Means | Look at |
|---|---|---|
NullPointerException | you used something that was null | the message — it names the variable |
ArrayIndexOutOfBoundsException | index outside the array | <= where you meant < |
ClassCastException | cast to a type it is not | the cast, and whether a generic is missing |
NumberFormatException | parsing a non-number | the input — the message quotes it |
ConcurrentModificationException | collection changed while iterating | a remove inside a loop |
StackOverflowError | infinite recursion | the repeating frame in the trace |
OutOfMemoryError | heap exhausted | an unbounded collection or cache |
Using a debugger
Print statements work, but a debugger answers questions printing cannot. The five things worth learning, in every IDE:
- Breakpoint — click the gutter next to a line. Execution pauses there and you can inspect every variable in scope.
- Step over (F8 in IntelliJ) — run the current line and stop on the next one.
- Step into (F7) — go inside the method being called.
- Resume (F9) — carry on to the next breakpoint.
- Evaluate expression — type any Java expression and run it against the paused state. This is the one people do not discover and it is the most useful.
Conditional breakpoints are what make debugging a loop bearable. Right-click a
breakpoint and give it a condition — order.getId().equals("A-1099") — and it pauses only
on the iteration you care about instead of all 10,000.
Related: an exception breakpoint pauses the moment a chosen exception is thrown,
anywhere, which is how you catch something being swallowed by a catch block you did not
write.
Logging that survives production
You cannot attach a debugger to a server at 3am, so logging is what you actually have. Use a
logging framework, not System.out:
class OrderService {
// In a real project this is SLF4J: private static final Logger log = LoggerFactory.getLogger(...)
void placeOrder(String orderId, double total) {
System.out.println("placing order id=" + orderId + " total=" + total);
try {
charge(total);
} catch (RuntimeException e) {
// Pass the exception as the LAST argument — that is what prints the stack trace
System.err.println("charge failed for order id=" + orderId);
e.printStackTrace();
throw e;
}
}
void charge(double total) { }
}
Three rules that make logs useful:
- Include identifiers. "Order failed" is useless; "order failed id=A-1099 customer=C-42" lets you find the one record that matters.
- Log the exception object, not just its message.
log.error("...", e)prints the whole trace including everyCaused by;log.error(e.getMessage())throws all of it away. - Use the levels.
ERRORfor things needing attention,WARNfor recoverable oddities,INFOfor milestones,DEBUGfor detail you can switch on. Everything atINFOmeans nothing is atINFO.
When there is no exception
The harder case: the program runs and produces the wrong answer. A debugger is at its best here. Two techniques:
Bisect the pipeline. Put a breakpoint halfway through and check whether the data is already wrong. If it is, the bug is in the first half; if not, the second. Repeat. Five or six rounds will localise a bug in a very large amount of code.
Check your assumptions explicitly. Most stubborn bugs turn out to be a belief that was never true — the list is not empty, the config was loaded, this method is called once. Print or inspect the thing you are certain about. It is the certain ones that are wrong.
Two JVM facts worth knowing
# What is this process doing right now? A thread dump, no restart required.
jstack <pid>
# What is on the heap? Useful when memory grows and will not come down.
jmap -histo <pid> | head -20
These ship with the JDK. jstack is the fastest way to diagnose a hung application —
it shows every thread and what it is waiting on, which usually names the deadlock or the blocking
call outright.
Next
Debugging tools are half of it. How to solve Java problems is next — the method to apply when you are stuck and the tools have not helped.