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)
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!");
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
isnull
in code which callsobj.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
returnsinfinity
just fine! - NullPointerException if a
null
Integer is unboxed to anint
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);
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)
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);
}
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
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
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.