Debugging Java
Reading a stack trace correctly and knowing your debugger's core moves — breakpoints, stepping, watches — turns most bugs from a mystery into a five-minute find.
Debugging is a skill independent of the language, but Java gives you a specific, well-defined toolkit for it: structured stack traces that name the exact call chain and exception cause, an IDE debugger (or the standalone jdb) with breakpoints and stepping, and JDK command-line tools (jstack, jmap, jcmd) for inspecting a running JVM from the outside. Knowing these tools well is often the difference between a bug that takes minutes to find and one that takes hours.
Reading a Stack Trace
When an uncaught exception propagates to the top of a thread, the JVM prints a stack trace: the exception class and message, followed by a list of frames — each naming a class, method, source file, and line number — from where the exception was thrown (top) down to where the call chain originated (bottom, closer to main).
Read it top-down for "exactly where did it throw": Calculator.java:12 inside divide is where the exception originated. Read it bottom-up for "how did we get there": main called compute, which called divide. The very first line after the exception type is its detail message (/ by zero here), which is often the fastest clue to what actually went wrong.
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.example.Calculator.divide(Calculator.java:12)
at com.example.Calculator.compute(Calculator.java:7)
at com.example.Main.main(Main.java:5)Exception Causes and Chaining
A stack trace can include a Caused by: section when one exception was thrown while handling another, via constructors like new RuntimeException(message, cause). The topmost exception in the printed trace is the one that was actually thrown last (often a wrapper); the Caused by: chain, read downward, leads to the original, root-cause exception — which is usually where the real bug is.
Preserving 'e' as the cause means the resulting stack trace shows both the high-level failure ("Failed to load configuration") and the actual root cause (FileNotFoundException) in a single trace, instead of losing the original exception's information.
public class ChainedException {
static void loadConfig() {
try {
parseFile();
} catch (java.io.IOException e) {
throw new IllegalStateException("Failed to load configuration", e); // chain the cause
}
}
static void parseFile() throws java.io.IOException {
throw new java.io.FileNotFoundException("config.yaml not found");
}
public static void main(String[] args) {
loadConfig();
}
}Exception in thread "main" java.lang.IllegalStateException: Failed to load configuration
at ChainedException.loadConfig(ChainedException.java:6)
at ChainedException.main(ChainedException.java:13)
Caused by: java.io.FileNotFoundException: config.yaml not found
at ChainedException.parseFile(ChainedException.java:10)
at ChainedException.loadConfig(ChainedException.java:4)
... 1 moreBreakpoints
A breakpoint tells the debugger to pause execution when a particular line is about to run (or, for a conditional breakpoint, only when an associated boolean expression evaluates to true). Set one by clicking in the gutter next to a line in most IDEs (IntelliJ IDEA, Eclipse, VS Code with a Java extension), then run the program in "debug mode" instead of "run mode" so the debugger attaches.
A conditional breakpoint (e.g. "pause only when i == 4237") is invaluable inside a loop that runs thousands of times but only misbehaves on one particular iteration — pausing on every iteration and manually stepping through would be impractical.
An exception breakpoint (supported by most IDE debuggers) pauses the instant a specified exception type is thrown, anywhere in the code — including inside library code — which is often faster than trying to guess where to place a line breakpoint for a bug you don't yet understand.
Stepping Through Code
Once paused at a breakpoint, the debugger offers several stepping actions: Step Over runs the current line and pauses on the next line in the same method (not descending into a called method's internals); Step Into descends into the method being called on the current line, letting you follow execution inside it; Step Out finishes executing the current method and pauses back in its caller; Resume/Continue runs until the next breakpoint (or program end) is reached.
A common habit: Step Over by default (to avoid drowning in library internals), and Step Into specifically when you suspect the bug is inside the method being called on the current line.
Step Over : execute this line, stay at this level -- don't follow into called methods
Step Into : follow execution into the method called on this line
Step Out : finish the current method, return to caller, pause there
Resume : run freely until the next breakpoint or program exitWatches and Variable Inspection
While paused, an IDE debugger shows local variables and their current values automatically in a "Variables" panel. A watch expression lets you additionally track a specific expression (a field access, a method call, a computed condition) across multiple pause points without re-typing it each time — useful for keeping an eye on, say, list.size() or this.state as you step through several breakpoints.
Most debuggers also support evaluating an arbitrary expression on demand while paused (an "Evaluate Expression" dialog), which can call methods and inspect nested object state live, without modifying and re-running the program.
Using the IDE Debugger End to End
A typical debugging session: reproduce the bug, set a breakpoint at (or just before) the point where you believe behavior diverges from expectation, run in debug mode, inspect variable state when execution pauses, and step forward line by line (or method by method) comparing actual values against what you expected at each point — the moment actual and expected diverge is where the bug lives.
For a program where you don't yet know where to look, starting with an exception breakpoint on the exception type you're seeing (or a breakpoint at the line named in the stack trace) is usually far faster than guessing and placing breakpoints speculatively.
Thread Dumps
A thread dump is a snapshot of every thread in a running JVM, showing each thread's state (RUNNABLE, BLOCKED, WAITING, TIMED_WAITING) and its current stack trace. Thread dumps are the primary tool for diagnosing a hung or deadlocked application: a thread stuck BLOCKED waiting on a lock another thread holds (and that other thread also blocked, in a cycle) is a deadlock, and the JVM's own thread-dump tooling explicitly detects and reports such cycles when found.
jstack <pid> # print a thread dump for a running JVM process
jcmd <pid> Thread.print # equivalent, via the general-purpose jcmd tool
# or: press Ctrl+\ (Unix) / send SIGQUIT to a running java process to dump to its own consoleHeap Dumps
A heap dump is a snapshot of every live object on the heap at a point in time — every object, its fields, and the reference chain keeping it reachable. Heap dumps are the primary tool for diagnosing memory leaks (steadily growing memory usage that never comes back down) and for understanding exactly what's consuming heap space, typically analyzed afterward in a tool such as Eclipse MAT or an IDE's built-in heap-dump viewer, not read directly.
Capturing a heap dump on a large heap is expensive (it can pause the application and produce a very large file), so it's typically done deliberately when investigating a specific memory issue, not routinely.
jmap -dump:live,format=b,file=heap.hprof <pid> # capture a heap dump to a file
jcmd <pid> GC.heap_dump heap.hprof # equivalent via jcmd
# JVM can also auto-dump on OutOfMemoryError with:
# -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprofjstack, jmap, jcmd at a High Level
These are JDK-bundled command-line diagnostic tools that attach to an already-running JVM process by its process ID, without requiring the process to have been started with any special debug flags:
jcmd is the modern, actively-developed umbrella tool and increasingly the recommended entry point; jstack and jmap remain widely used and are still bundled, but newer diagnostic capabilities tend to land in jcmd first.
jps : lists running JVM processes and their PIDs (a Java-aware 'ps')
jstack <pid> : prints a thread dump (stack trace + state for every thread)
jmap <pid> : heap-related info; jmap -dump for a full heap dump, -histo for a live object-count/size histogram
jcmd <pid> <cmd> : general-purpose diagnostic command tool; can do most of what jstack/jmap do
and more (jcmd <pid> help lists available commands for that JVM)Practical Usage: Logging as a Debugging Complement
A debugger is excellent for a reproducible bug you can pause and step through interactively. For an intermittent bug, a production-only bug, or a bug in a system where attaching a debugger isn't practical, well-placed logging (see the Logging topic) at the right level, showing the actual values flowing through the suspect code path, is often the more practical tool — the two techniques complement rather than replace each other.
Edge Cases
A breakpoint inside a lambda or a stream pipeline can be harder to reason about than a breakpoint in ordinary sequential code, because the call stack shown will include internal Streams/lambda machinery frames — modern IDE debuggers generally handle this reasonably well, but the stepping experience can feel less linear than in a plain loop.
A stack trace can be truncated (... N more at the bottom of a Caused by: section) when frames are shared with the enclosing trace — this is normal and not missing information, just a de-duplication of identical frames already shown above.
Debugging multi-threaded code by single-stepping one thread can itself change the bug's timing (a classic "heisenbug") since pausing one thread changes the relative scheduling of others — thread dumps and careful logging are often more reliable for genuinely timing-sensitive concurrency bugs than step-debugging.
Quick Reference
Stack trace : read top-down for "where", bottom-up for "call chain"; Caused by: is the real root cause
Breakpoint : pause at a line; conditional breakpoint pauses only when an expression is true
Step Over/Into/Out/Resume : the four core stepping actions
Watch : track an expression's value across pause points
jps : list running JVM processes
jstack <pid> : thread dump -- diagnose hangs/deadlocks
jmap -dump : heap dump -- diagnose memory leaks
jcmd <pid> <cmd> : general-purpose diagnostic command tool (modern umbrella)Key Takeaways
Read a stack trace top-down to find exactly where an exception was thrown, and bottom-up to trace the call chain that led there; a Caused by: chain leads to the root-cause exception.
Breakpoints (including conditional and exception breakpoints) pause execution for interactive inspection; Step Over/Into/Out/Resume are the core navigation actions once paused.
Watches let you track a specific expression's value across multiple pause points without re-inspecting it manually each time.
Thread dumps (jstack, jcmd Thread.print) diagnose hangs and deadlocks by showing every thread's state and stack; heap dumps (jmap -dump, jcmd GC.heap_dump) diagnose memory leaks by showing every live object and its reference chain.
jcmd is the modern, general-purpose umbrella diagnostic tool; jstack and jmap remain widely used, JDK-bundled specialists for thread and heap inspection respectively.
Interactive debugging and logging are complementary: debuggers suit reproducible bugs you can pause and step through; logging suits intermittent or production-only issues.
catch (IOException e) { throw new RuntimeException("failed to read file"); // original exception dropped } catch (IOException e) { throw new RuntimeException("failed to read file", e); // cause preserved }
Without passing e as the cause, the resulting stack trace loses all information about the original exception -- exactly the detail (which file, which underlying I/O error) needed to actually debug the failure.
// staring at a loop of 50,000 iterations with a plain breakpoint on line 10, clicking Resume 4,236 times
// set a conditional breakpoint on line 10 with condition: i == 4237
A conditional breakpoint pauses execution only when its expression is true, jumping straight to the specific iteration of interest instead of manually resuming through thousands of irrelevant ones.
// application appears frozen; developer restarts it without investigating jstack <pid> // capture a thread dump first, then diagnose (or restart if urgent, but keep the dump)
Restarting a hung process destroys the exact evidence (thread states, blocked-lock chains) needed to diagnose why it hung; capturing a thread dump first preserves that evidence for analysis even if a restart is still necessary.
When reading a stack trace, which direction do you read to find exactly where the exception was thrown, and which direction to trace the call chain that led there?
Top-down (starting right after the exception type/message) shows exactly where it was thrown; bottom-up (toward main) shows the sequence of calls that led to that point.
What does a 'Caused by:' section in a stack trace represent, and why is it usually the most important part to investigate?
It shows an earlier exception that was caught and wrapped in a new exception (via a constructor taking a cause); it's usually the most important part because it's the actual root-cause failure, while the outer, top-level exception is often just a higher-level wrapper describing the consequence.
What is the difference between Step Over and Step Into while paused at a breakpoint?
Step Over executes the current line and pauses on the next line at the same call-stack level, without following execution into any method called on that line; Step Into follows execution down into the method being called, letting you inspect its internals.
How does a conditional breakpoint differ from a regular breakpoint, and when is it especially useful?
A conditional breakpoint only pauses execution when an associated boolean expression evaluates to true, rather than on every hit; it's especially useful inside a loop that runs many times but only misbehaves on a specific iteration, letting you jump straight to that iteration instead of manually resuming through every prior one.
What tool would you use to diagnose a Java application that appears to be hung, and what specifically would you look for in its output?
jstack <pid> (or jcmd <pid> Thread.print) to capture a thread dump; you'd look for threads in BLOCKED state waiting on a lock, and specifically for a cycle of threads each blocked waiting on a lock the next one holds -- a deadlock, which the tool typically calls out explicitly when detected.
What is a heap dump used for, and what tool captures one?
A heap dump is a snapshot of every live object on the JVM heap along with its fields and reference chains, used primarily to diagnose memory leaks and understand what's consuming heap space; it's captured with jmap -dump (or jcmd <pid> GC.heap_dump) and typically analyzed afterward in a tool like Eclipse MAT.
Why might single-step debugging be less reliable for diagnosing a multi-threaded timing bug than thread dumps or logging?
Pausing one thread at a breakpoint changes the relative scheduling and timing of other threads (a 'heisenbug' effect), which can mask or alter the very race condition being investigated; thread dumps (a snapshot with minimal interference) and careful logging tend to preserve the actual timing behavior better.
Based only on this stack trace, where exactly is the bug located, and what is the most likely fix?
The NPE originated at Greeter.java line 8, inside the greet method, where a local variable or parameter named 'name' was null when .length() was called on it (the modern helpful NPE message names the exact null reference and the exact call). The most likely fix is to trace back to line 4 in Main.java to see what value was passed to greet(...), and either ensure a non-null name is always supplied, or add an explicit null check inside greet (e.g. Objects.requireNonNull(name)) so the failure is caught with a clearer message at the entry point.
Write a short Java snippet demonstrating exception chaining: a method loadUser that catches a low-level SQLException and rethrows it wrapped in a custom UserLoadException, preserving the original as the cause.
class UserLoadException extends RuntimeException { UserLoadException(String message, Throwable cause) { super(message, cause); } } public class UserLoader { static void loadUser(int id) { try { querySql(id); // pretend this throws SQLException } catch (java.sql.SQLException e) { throw new UserLoadException("Failed to load user " + id, e); } } static void querySql(int id) throws java.sql.SQLException { throw new java.sql.SQLException("connection timed out"); } }
By passing 'e' as the second constructor argument (the cause), the resulting stack trace for UserLoadException will include a 'Caused by: java.sql.SQLException: connection timed out' section beneath the high-level message, giving a debugger both the business-level context (which user failed to load) and the precise low-level root cause (a connection timeout), instead of losing the SQLException's information entirely.
You suspect a deadlock in a running Java application with PID 12345. Write the command-line steps you'd use to confirm it and see which threads and locks are involved.
jstack (or jcmd Thread.print) prints every thread's state and full stack trace; for a genuine deadlock, the JVM's own thread-dump output typically includes an explicit 'Found one Java-level deadlock' section naming the threads involved and the locks each is waiting on versus holding, making the cycle immediately visible without manually cross-referencing every thread's BLOCKED state by hand.
Describe the most efficient debugger technique to inspect exactly what's different about the item at i == 8734, without manually resuming thousands of times.
Set a conditional breakpoint on the process(current) line with the condition 'i == 8734' (or equivalently on the Item current = items.get(i); line). The debugger then runs freely through all prior iterations and pauses exactly once, right at the problematic iteration, where you can inspect 'current' in the Variables panel or via a watch expression to see what makes it different from the items that processed successfully.
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
at com.example.Greeter.greet(Greeter.java:8)
at com.example.Main.main(Main.java:4)for (int i = 0; i < items.size(); i++) {
Item current = items.get(i);
process(current); // fails only around i == 8734, out of 20000 items
}