The mystery of the endless Throwable's cause chain shown in the IntelliJ debugger
A Throwable
in Java can have a cause which in turn is a Throwable
as well. This cause can have a cause and we can have a chain of Throwable
objects which we can follow until a Throwable
has no cause. Let me explain this with a small example:
1class Scratch {
2 public static void main(String[] args) {
3 try {
4 try {
5 int division = divide(4, 0);
6 } catch (Exception e) {
7 throw new IllegalArgumentException("got exception calling divide", e);
8 }
9 } catch (Throwable t) {
10 while (t != null) {
11 System.out.println(t.getClass().getCanonicalName());
12 t = t.getCause();
13 }
14 }
15 }
16
17 private static int divide(int divident, int divisor) {
18 return divident / divisor;
19 }
20}
In this example we call a function to divide two numbers and pass in a divisor with value 0. This of course leads to an ArithmeticException
which we catch and set as the cause for a new IllegalArgumentException
. We then catch this new exception and print out the cause-chain . Running this program gives the expected result:
java.lang.IllegalArgumentException
java.lang.ArithmeticException
Process finished with exit code 0
No let’s debug this program in IntelliJ and stop at line ten, and lets examine the value of t
:
What’s this? We see the IllegalArgumentException
and then the ArithmeticException
. But then the cause of the ArithmeticException
seems to be the very same object creating an endless chain of causes. We did not observe this in the code, so why does the debugger display this information?
To understand this we need to look at the Throwable
class and it’s cause
property. The cause of a Throwable
can be either set as constructor argument or by calling the method initCause(Throwable)
on an existing throwable. The Javadoc for initCause() states that the method throws an exception if the parameter is either the object itself (A throwable cannot be it’s own cause) or if a cause was already set. So how does the throwable know if it’s cause was already set? The trick for this is that the cause
property is initialized to this
in the field’s initializer:
/**
* The throwable that caused this throwable to get thrown, or null if this
* throwable was not caused by another throwable, or if the causative
* throwable is unknown. If this field is equal to this throwable itself,
* it indicates that the cause of this throwable has not yet been
* initialized.
*
* @serial
* @since 1.4
*/
private Throwable cause = this;
The getCause()
method returns null if the cause was not initialized, otherwise the value that was set:
public synchronized Throwable getCause() {
return (cause==this ? null : cause);
}
And the code to set a cause checks this as well:
public synchronized Throwable initCause(Throwable cause) {
if (this.cause != this)
throw new IllegalStateException("Can't overwrite cause with " +
Objects.toString(cause, "a null"), this);
if (cause == this)
throw new IllegalArgumentException("Self-causation not permitted", this);
this.cause = cause;
return this;
}
So when using getCause()
to follow the chain we end at a Throwable
that has either no cause set or where the cause was explicitly set to null.
And the debugger? The debugger does not call the getCause()
method but uses introspection of the exception and directly reads the value of the cause property. And this is the inspected object again leading to this endless chain of causes that are displyed.