Understanding Java exception stacktraces

I've seen a lot of people asking questions in forums and Q&A sites about how to fix errors in their Java code which are actually easily fixable if one could understand the stacktrace output.

What is a stacktrace?

A stacktrace is the ordered list of the calls made until a given point of execution (line of code) in the current thread. For example, given this code:

class A {
    public void print() {
        new B().print();
        System.out.println("A.print called");
    }
}

class B {
    public void print() {
        System.out.println("B.print called");
    }
}

public class Main {
    public static void main(String[] args) { 
        new A().print();
    }
}

if we put a breakpoint at line 11 System.out.println("B.print called"); we can check the stacktrace (Eclipse screenshot):

Which means we're executing B.print() at line 11, which was called from A.print() at line 4, which in turn was called from Main.main(String[]) at line 19.

Exception stacktrace

In case of an exception, the stacktrace corresponds of the line of code that has thrown this exception. So if we change the above B.print() like

public void print() {
    final String a = null;
    System.out.println("B.print called and " + a.toUpperCase());
}

it's obvious we're going to get a NullPointerException because a is null at a.toUpperCase().

Exception in thread "main" java.lang.NullPointerException
    at B.print(Main.java:12)
    at A.print(Main.java:4)
    at Main.main(Main.java:20)

So as said, the stacktrace is indicating us where the thrown exception is happening, and also very important: from exactly where this was called. This is extremely useful when debugging, because it allows us to trace back (hence the name) the root of the error and what piece of code is the responsible of it. To show an example of this, let's modify the previous A and B classes so they look like so

class A {
    public void print() {
        final B b = new B();
        b.print("Hello");
        b.print(null);  
    }
}

class B {
    /**
    * Prints the parameter as uppercase.
    * @param toPrint String to print. Cannot be null.
    */
    public void print(final String toPrint) {
        System.out.println(toPrint.toUpperCase());
    }
}

Now let's execute and see the NPE stacktrace:

Exception in thread "main" java.lang.NullPointerException
    at B.print(Main.java:12)
    at A.print(Main.java:6)
    at Main.main(Main.java:19)

So the NPE is happening at B.print(Main.java:12), which is System.out.println(toPrint.toUpperCase());. But this line is not wrong. So we go up in the stacktrace: A.print(Main.java:6), which is b.print(null);. This line is an error because B.print(String) JavaDoc clearly states that the parameter cannot be null. So we found our error. Be sure to read any related JavaDoc for the methods you're using, they sometimes contain useful hints about how to (and how not to) use them.

Caused by

The "Caused by" in the stacktrace refers to which wrapped exceptions are inside the currently thrown exception. This is available through the Exception(Throwable) and Exception(Exception) constructors. To understand how this works, let's see it through an example. Modify the above A and B classes like

class A {
    public void print() {
        final B b = new B();
        try {
            b.print("Hello");
        } catch (final UnsupportedOperationException e) {
            throw new AnotherException(e);
        }
    }
}

class B {
    public void print(final String toPrint) {
        throw new UnsupportedOperationException();
    }
}

And add the following class

class AnotherException extends RuntimeException {
    public AnotherException(final Exception e) {
        super(e);
    }
}

So as you can see at B.print(String) line 20, we're throwing an UnsupportedOperationException. But A.print() is catching this exception and throwing a new one: AnotherException. It uses the Exception(Exception) constructor, which wraps one exception into another one. So if we run now our code, we will have the following stacktrace:

Exception in thread "main" AnotherException: java.lang.UnsupportedOperationException
    at A.print(Main.java:14)
    at Main.main(Main.java:28)
Caused by: java.lang.UnsupportedOperationException
    at B.print(Main.java:21)
    at A.print(Main.java:12)
    ... 1 more

So here the really thrown exception is AnotherException thrown in A.print(), but the real cause is actually the UnsupportedOperationException thrown in B.print(String).

I hope this explained a bit what stacktraces are and how extremely useful they are for debugging our code errors. See you soon!

Comments

Popular Posts