Understanding Stack Traces and Debugging them Further
Reading stack traces is directly related to your experience with a specific project. But there are some things you can do to debug the findings
Recently a junior developer sent me an obfuscated stack trace and was pretty surprised when I instantly knew the problem and pointed him at the necessary change. To be fair, I had the advantage of being the person who put that bug there in the first place… But still the ability to glean information from a stack trace, even an obfuscated one, is a serious skill.
The stack trace in question was a ClassNotFoundException
, that’s typically pretty easy and already tells you everything you need to know. The class isn’t there.Why it isn’t there is really a matter of what we did wrong. In this case since the project was obfuscated the bug was that this class wasn’t excluded from obfuscation.
Despite all the hate it got over the years, NullPointerException
is one of my favorite exceptions. You instantly know what happened and in most cases the stack leads almost directly to the problem. There are some edge cases e.g.:
COPY
myList.get(offset).invokeMethod();
So which one triggered the NullPointerException
?
If you’re using the latest version of Java it would tell you, which is pretty cool. But if we’re still at Java 11 (or 8) there’s more than one option. At least 3 or maybe 4 if we cheat a bit:
myList
is the obvious one but it rarely is null and if so you would see it immediatelyoffset can be null. It can be an Integer object in which case it can be
null
due to autoboxing. It’s also less likelyThe most likely culprit in this specific case is the return value from the
get()
method which means one of the list elements isnull
Finally
invokeMethod
itself can throw aNullPointerException
. But that’s a bit of cheating since the stack will be a bit deeper
So without knowing anything about the code we can pretty much guess what failed at a line just by knowing the exception type. But this doesn’t lead us directly to the bug in all cases.
There Was a Null
That NullPointerException
probably happened due to a null in the list. Assuming you verified that you might still not know how that null got into the list in the first place…
This isn’t hard to find out, let’s assume that the List is of the ArrayList
type, in that case just open the ArrayList
class (which you can do with Control-O in IntelliJ) and place a conditional breakpoint on the add()
method. You can test if the value is null and that will stop at a breakpoint if someone tries to add null to the list.
Now this won’t catch all the cases of null
sneaking into the list. It can do so via the stream API, via addAll()
and a couple of other methods. The nice thing is that we can grab pretty much any one of those methods:
Since addAll()
accepts a Collection
we can use the standard contains()
method to check if we have a null
element in the Collection
and if so stop.
TL;DR
In many cases we can glean the cause of an exception we see in the log or get from the user by just reviewing the stack trace and digging deeper. Obviously, keep in mind nested exceptions and other such issues.
The debugger can still be a great ally when trying to figure out the root cause of an exception stack. We can leverage features like conditional breakpoints to narrow this down. Surprisingly, we didn’t touch on exception breakpoints in this post. I think they have their value but when we have a stack we already know (roughly) what happened and where.
We need something that goes beyond that and I tried to cover that in this article.