Debugging

August 17, 20264 min readUpdated 8/20/2026

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:

  1. The message. Since Java 14, a NullPointerException names the thing that was null and what you tried to do with it — "because name is null". That sentence usually is the answer.
  2. The top frame is where it was thrown: OrderService.java:42.
  3. Reading down tells you how you got there. main called placeOrder, which called validate.
  4. 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

ExceptionMeansLook at
NullPointerExceptionyou used something that was nullthe message — it names the variable
ArrayIndexOutOfBoundsExceptionindex outside the array<= where you meant <
ClassCastExceptioncast to a type it is notthe cast, and whether a generic is missing
NumberFormatExceptionparsing a non-numberthe input — the message quotes it
ConcurrentModificationExceptioncollection changed while iteratinga remove inside a loop
StackOverflowErrorinfinite recursionthe repeating frame in the trace
OutOfMemoryErrorheap exhaustedan 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 every Caused by; log.error(e.getMessage()) throws all of it away.
  • Use the levels. ERROR for things needing attention, WARN for recoverable oddities, INFO for milestones, DEBUG for detail you can switch on. Everything at INFO means nothing is at INFO.

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.