How to read and understand a Java Stacktrace

Matthew Gilliard - Jun 3 '19 - - Dev Community

When things go wrong in a running Java application, often the first sign you will have is lines printed to the screen that look like this:

Exception in thread "main" java.lang.RuntimeException: Something has gone wrong, aborting!
  at com.myproject.module.MyProject.badMethod(MyProject.java:22)
  at com.myproject.module.MyProject.oneMoreMethod(MyProject.java:18)
  at com.myproject.module.MyProject.anotherMethod(MyProject.java:14)
  at com.myproject.module.MyProject.someMethod(MyProject.java:10)
  at com.myproject.module.MyProject.main(MyProject.java:6)
Enter fullscreen mode Exit fullscreen mode

This is a Stacktrace , and in this post I'll explain what they are, how they are made and how to read and understand them. If that looks painful to you then read on...

Anatomy of a Stacktrace

Usually a Stacktrace is shown when an Exception is not handled correctly in code. This may be one of the built-in Exception types, or a custom Exception created by a program or a library.

The Stacktrace contains the Exception’s type and a message , and a list of all the method calls which were in progress when it was thrown.

Let’s dissect that Stacktrace. The first line tells us the details of the Exception:

This is a good start. Line 2 shows what code was running when that happened:

That helps us narrow down the problem, but what part of the code called badMethod? The answer is on the next line down, which can be read in the exact same way. And how did we get there? Look on the next line. And so on, until you get to the last line, which is the main method of the application. Reading the Stacktrace from bottom to top you can trace the exact path from the beginning of your code, right to the Exception.

What went wrong?

The thing that causes an Exception is usually an explicit throw statement. Using the file name and line number you can check exactly what code threw the Exception. It will probably look something like this:

  throw new RuntimeException("Something has gone wrong, aborting!");
Enter fullscreen mode Exit fullscreen mode

This is a great place to start looking for the underlying problem: are there any if statements around that? What does that code do? Where does the data used in that method come from?

It is also possible for code to throw an Exception without an explicit throw statement, for example you can get:

  • NullPointerException if obj is null in code which calls obj.someMethod()
  • ArithmeticException if a division by zero happens in integer arithmetic, ie 1/0 - curiously there is no Exception if this is a floating-point calculation though, 1.0/0.0 returns infinity just fine!
  • NullPointerException if a null Integer is unboxed to an int in code like this: Integer a=null; a++;
  • There are some other examples in the Java Language Specification, so it’s important to be aware that Exceptions can arise without being explicitly thrown.

Dealing with Exceptions Thrown by Libraries

One of the great strengths of Java is the huge number of libraries available. Any popular library will be well tested so generally when faced with an Exception from a library, it’s best to check first whether the error is caused by how our code uses it.

For example, if we're using the Fraction class from Apache Commons Lang and pass it some input like this:

  Fraction.getFraction(numberOfFoos, numberOfBars);
Enter fullscreen mode Exit fullscreen mode

If numberOfBars is zero, then the Stacktrace will be like this:

Exception in thread "main" java.lang.ArithmeticException: The denominator must not be zero
  at org.apache.commons.lang3.math.Fraction.getFraction(Fraction.java:143)   
  at com.project.module.MyProject.anotherMethod(MyProject.java:17)
  at com.project.module.MyProject.someMethod(MyProject.java:13)
  at com.project.module.MyProject.main(MyProject.java:9)
Enter fullscreen mode Exit fullscreen mode

Many good libraries provide Javadoc which includes information about what kinds of Exceptions may be thrown and why. In this case Fraction.getFraction has documented will throw an ArithmeticException if a Fraction has a zero denominator. Here it's also clear from the message, but in more complicated or ambiguous situations the docs can be a great help.

To read this Stacktrace, start at the top with the Exception's type - ArithmeticException and message The denominator must not be zero. This gives an idea of what went wrong, but to discover what code caused the Exception, skip down the Stacktrace looking for something in the package com.myproject (it’s on the 3rd line here), then scan to the end of the line to see where the code is (MyProject.java:17). That line will contain some code that calls Fraction.getFraction. This is the starting point for investigation: What is passed to getFraction? Where did it come from?

In big projects with many libraries, Stacktraces can be hundreds of lines long so if you see a big Stacktrace, practise scanning the list of at ... at ... at ... looking for your own code - it’s a useful skill to develop.

Best Practice: Catching and Rethrowing Exceptions

Let's say we are working on a big project that deals with fictional FooBars, and our code is going to be used by others. We might decide to catch the ArithmeticException from Fraction and re-throw it as something project-specific, which looks like this:

  try {
    ....
    Fraction.getFraction(x,y);
    ....
  } catch ( ArithmeticException e ){ 
    throw new MyProjectFooBarException("The number of FooBars cannot be zero", e);
  }
Enter fullscreen mode Exit fullscreen mode

Catching the ArithmeticException and rethrowing it has a few benefits:

  • Our users are shielded from having to care about the ArithmeticException - giving us flexibility to change how commons-lang is used.
  • More context can be added, eg stating that it’s the number of FooBars that is causing the problem.
  • It can make Stacktraces easier to read, too, as we’ll see below.

It isn't necessary to catch-and-rethrow on every Exception, but where there seems to be a jump in the layers of your code, like calling into a library, it often makes sense.

Notice that the constructor for MyProjectFooBarException takes 2 arguments: a message and the Exception which caused it. Every Exception in Java has a cause field, and when doing a catch-and-rethrow like this then you should always set that to help people debug errors. A Stacktrace might now look something like this:

Exception in thread "main" com.myproject.module.MyProjectFooBarException: The number of FooBars cannot be zero
  at com.myproject.module.MyProject.anotherMethod(MyProject.java:19)
  at com.myproject.module.MyProject.someMethod(MyProject.java:12)
  at com.myproject.module.MyProject.main(MyProject.java:8)
Caused by: java.lang.ArithmeticException: The denominator must not be zero
  at org.apache.commons.lang3.math.Fraction.getFraction(Fraction.java:143)
  at com.myproject.module.MyProject.anotherMethod(MyProject.java:17)
  ... 2 more
Enter fullscreen mode Exit fullscreen mode

The most recently thrown Exception is on the first line, and the location where it was thrown is still on line 2. However, this type of Stacktrace can cause confusion because the catch-and-rethrow has changed the order of method calls compared to the Stacktraces we saw before. The main method is no longer at the bottom, and the code which first threw an Exception is no longer at the top. When you have multiple stages of catch-and-rethrow then it gets bigger but the pattern is the same:

Check the sections from first to last looking for your code, then read relevant sections from bottom to top.

Libraries vs Frameworks

The difference between a Library and a Framework in Java is:

  • Your code calls methods in a Library
  • Your code is called by methods in a Framework

A common type of Framework is a web application server, like SparkJava or Spring Boot. Using SparkJava and Commons-Lang with our code we might see a Stacktrace like this:

com.framework.FrameworkException: Error in web request
    at com.framework.ApplicationStarter.lambda$start$0(ApplicationStarter.java:15)
    at spark.RouteImpl$1.handle(RouteImpl.java:72)
    at spark.http.matching.Routes.execute(Routes.java:61)
    at spark.http.matching.MatcherFilter.doFilter(MatcherFilter.java:134)
    at spark.embeddedserver.jetty.JettyHandler.doHandle(JettyHandler.java:50)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1568)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
    at org.eclipse.jetty.server.Server.handle(Server.java:503)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:364)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:260)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
    at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:118)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: com.project.module.MyProjectFooBarException: The number of FooBars cannot be zero
    at com.project.module.MyProject.anotherMethod(MyProject.java:20)
    at com.project.module.MyProject.someMethod(MyProject.java:12)
    at com.framework.ApplicationStarter.lambda$start$0(ApplicationStarter.java:13)
    ... 16 more
Caused by: java.lang.ArithmeticException: The denominator must not be zero
    at org.apache.commons.lang3.math.Fraction.getFraction(Fraction.java:143)
    at com.project.module.MyProject.anotherMethod(MyProject.java:18)
    ... 18 more
Enter fullscreen mode Exit fullscreen mode

OK that is getting quite long now. As before we should suspect our own code first, but it's getting harder and harder to find where that is. At the top there is the Framework's Exception, at the bottom the Library's and right in the middle is our own code. Phew!

A complex Framework and Library can create a dozen or more Caused by: sections, so a good strategy is to jump down those looking for your own code: Caused by: com.myproject... Then read that section in detail to isolate the problem.

In Summary

Learning how to understand Stacktraces and read them quickly will let you home in on problems and makes debugging much less painful. It's a skill which improves with practise, so next time you see a big Stacktrace don't be intimidated - there is a lot of useful information there if you know how to extract it.

If you have any tips or tricks about dealing with Java Stacktraces I'd love to hear about them so get in touch with me and let's share what we know.

mgilliard@twilio.com
@MaximumGilliard

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .