Understanding and Leveraging the Java Stack Trace

By: Eugen
  |  March 17, 2023
Understanding and Leveraging the Java Stack Trace

Stack traces are probably one of the most common things you’re regularly running into while working as a Java developer. When unhandled exceptions are thrown, stack traces are simply printed to the console by default.

Nevertheless, it’s easy to only have a surface-level understanding of what these are and how to use them. This article will shed light on the subject.

What is a Stack Trace?

Simply put, a stack trace is a representation of a call stack at a certain point in time, with each element representing a method invocation. The stack trace contains all invocations from the start of a thread until the point it’s generated. This is usually a position at which an exception takes place.

A stack trace’s textual form like this should look familiar:

Exception in thread "main" java.lang.RuntimeException: A test exception
  at com.stackify.stacktrace.StackTraceExample.methodB(StackTraceExample.java:13)
  at com.stackify.stacktrace.StackTraceExample.methodA(StackTraceExample.java:9)
  at com.stackify.stacktrace.StackTraceExample.main(StackTraceExample.java:5)

When printed out, the generation point shows up first, and method invocations leading to that point are displayed underneath. This printing order makes sense because when an exception occurs, you want to look at the most recent methods first. These methods are likely to contain the root cause of the failure rather than those far away.

The rest of this article will take an in-depth look at stack traces, starting with the StackTraceElement class. Each instance of this class indicates an element in a stack trace.

The Stack Walking API, introduced in Java 9 to provide a more flexible mechanism to traverse call stacks, will be covered as well.

The StackTraceElement Class

A stack trace consists of stack trace elements. Before Java 9, the only way to denote such elements is to use the StackTraceElement class.

Accessible Information

A StackTraceElement object provides you with access to basic data on a method invocation, including the names of the class and method where that invocation occurs. You can retrieve this info using these straightforward APIs:

  • getClassName – returns the fully qualified name of the class containing the method invocation
  • getMethodName – returns the name of the method containing the method invocation

Starting with Java 9, you can also obtain data on the containing module of a stack frame – using the getModuleName and getModuleVersion methods.

Thanks to the SourceFile and LineNumberTable attributes in the class file, the corresponding position of a frame in the source file is identifiable as well. This information is very helpful for debugging purposes:

  • getFileName – returns the name of the source file associated with the class containing the method invocation
  • getLineNumber – returns the line number of the source line containing the execution point

For a complete list of methods in the StackTraceElement class, see the Java API documentation.

Before moving on to a couple of methods that you can use to obtain elements of a stack trace, take a look at the skeleton of a simple example class:

package com.stackify.stacktrace;
public class StackElementExample {
    // example methods go here
}

This class will contain methods illustrating a stack trace.

The following test class will be filled with methods calling those in the StackElementExample class:

package com.stackify.stacktrace;
// import statements
public class StackElementExampleTest {
    // test methods go here
}

Accessing Stack Traces with the Thread Class

You can obtain a stack trace from a thread – by calling the getStackTrace method on that Thread instance. This invocation returns an array of StackTraceElement, from which details about stack frames of the thread can be extracted.

The following are two methods of the StackElementExample class. One of them calls the other, hence both become part of the same call stack:

public StackTraceElement[] methodA() {
    return methodB();
}
public StackTraceElement[] methodB() {
    Thread thread = Thread.currentThread();
    return thread.getStackTrace();
}

The first element in the stack trace created in methodB is the invocation of the getStackTrace method itself. The second element, at index 1, is the method enclosing that invocation.

Here’s a quick test that verifies the class and method names:

@Test
public void whenElementOneIsReadUsingThread_thenMethodUnderTestIsObtained() {
    StackTraceElement[] stackTrace = new StackElementExample().methodA();
    StackTraceElement elementOne = stackTrace[1];
    assertEquals("com.stackify.stacktrace.StackElementExample", elementOne.getClassName());
    assertEquals("methodB", elementOne.getMethodName());
}

When a test method calls methodA in the example class, which in turn calls methodB, that test method should be two elements away from methodB in the stack:

@Test
public void whenElementThreeIsReadUsingThread_thenTestMethodIsObtained() {
    StackTraceElement[] stackTrace = new StackElementExample().methodA();
    StackTraceElement elementThree = stackTrace[3];
    assertEquals("com.stackify.stacktrace.StackElementExampleTest", elementThree.getClassName());
    assertEquals("whenElementThreeIsReadUsingThread_thenTestMethodIsObtained", elementThree.getMethodName());
}

Accessing Stack Traces with the Throwable Class

When the program throws a Throwable instance, instead of simply printing the stack trace on the console or logging it, you can obtain an array of StackTraceElement objects by calling the getStackTrace method on that instance. The signature and the return value of this method are the same as those of the method in the Thread class you have gone through.

Here are two methods featuring the throwing and handling of a Throwable object:

public StackTraceElement[] methodC() {
    try {
        methodD();
    } catch (Throwable t) {
        return t.getStackTrace();
    }
    return null;
}
public void methodD() throws Throwable {
    throw new Throwable("A test exception");
}

When the Throwable is thrown, a stack trace is generated at the point where the problem occurs. As a result, the first element of the stack is the method containing the throwing:

@Test
public void whenElementZeroIsReadUsingThrowable_thenMethodThrowingThrowableIsObtained() {
    StackTraceElement[] stackTrace = new StackElementExample().methodC();
    StackTraceElement elementZero = stackTrace[0];
    assertEquals("com.stackify.stacktrace.StackElementExample", elementZero.getClassName());
    assertEquals("methodD", elementZero.getMethodName());
}

And the second is the method that handles the Throwable:

@Test
public void whenElementOneIsReadUsingThrowable_thenMethodCatchingThrowableIsObtained() {
    StackTraceElement[] stackTrace = new StackElementExample().methodC();
    StackTraceElement elementOne = stackTrace[1];
    assertEquals("com.stackify.stacktrace.StackElementExample", elementOne.getClassName());
    assertEquals("methodC", elementOne.getMethodName());
}

If you were to change the body of the catch block in methodC to a trivial handling:

t.printStackTrace();

you would see the textual representation of the stack trace:

java.lang.Throwable: A test exception
  at com.stackify.stacktrace.StackElementExample.methodD(StackElementExample.java:23)
  at com.stackify.stacktrace.StackElementExample.methodC(StackElementExample.java:15)
  at com.stackify.stacktrace.StackElementExampleTest
    .whenElementOneIsReadUsingThrowable_thenMethodCatchingThrowableIsObtained(StackElementExampleTest.java:34)
...

As you can see, the text output reflects the StackTraceElement array.

The Stack Walking API

One of the prominent features of Java 9 is the Stack Walking API. This section will go over the driving forces behind the introduction of this API, and how to use it to traverse stack traces.

Drawbacks of StackStraceElement

A StackTraceElement object provides more information than a single line in the textual representation of a stack trace. However, each piece of data – such an object stores – is still in a simple form: a String or a primitive value; it doesn’t reference a Class object. Consequently, it’s not easy to use information from a stack trace in the program.

Another problem with the old way of retrieving stack traces is that you cannot ignore frames that you don’t need. On the other hand, you may lose useful elements as the JVM may skip some frames for the performance. In the end, it’s possible to have elements you don’t want and don’t have some you actually need.

The Stack Walking API to the Rescue

The Stack Walking API provides a flexible mechanism to traverse and extract information from call stacks, allowing you to filter, then access frames, in a lazy manner. This API works around the StackWalker class, which encloses two inner types: StackFrame and Option.

Stack Frames

An instance of the StackFrame interface represents an individual frame in a stack, much like what a StackTraceElement object does. As you’d expect, this interface defines a number of APIs, similar to those in the StackTraceElement class, e.g. getMethodName or getLineNumber.

And, if you need to, you can convert a StackFrame object to StackTraceElement by calling the method toStackTraceElement.

However, there is an important API that makes StackFrame a better choice than StackTraceElement – namely getDeclaringClass. This method returns a Class instance, enabling you to perform more complex operations than what you could do with a simple class name. However, do note this is only applicable if the stack walker is set up to retain Class objects.

The next subsection will go over the options you can set for such a stack walker.

Stack Walker Options

Instances of the Option enum type can be used to determine the information retrieved by a stack walker.

Here’s a complete list of its constants:

  • RETAIN_CLASS_REFERENCE – retains the Class object in each stack frame during a stack walk
  • SHOW_REFLECT_FRAMES – shows all reflection frames
  • SHOW_HIDDEN_FRAMES – shows all hidden frames, including reflection frames

The StackWalker Class

The StackWalker class is the entry point to the Stack Walking API. This class doesn’t define public constructors; you must use one of the overloading static methods, named getInstance, to create its objects.

You can have a StackWalker with the default configuration by calling getInstance with no arguments. This configuration instructs the stack walker to retain no class references and omit all hidden frames.

You can also pass an Option constant to that method. In case multiple options are provided, they must be wrapped in a Set before being used to construct a stack walker.

The most noticeable method of StackWalker is the walk method. This method applies a Function to the stream of StackFrame objects, starting from the top frame where the invocation of the walk method occurs.

The frame stream is closed when the walk method returns, and it does so for good reason. Since the JVM is free to reorganize the stack for the performance, the result would be inaccurate if you accessed the stream after the walk method completed.

You can also use a derivative of the walk method, namely forEach. This method performs a Consumer on elements of the StackFrame stream.

Notice that the StackWalker class is thread-safe. Multiple threads can share a single StackWalker instance to go through their own stack without causing any concurrency issues.

To illustrate the Stack Walking API, let’s have a look at this simple class:

package com.stackify.stacktrace;
public class StackWalkingExample {
    // example methods go here
}

And this test class:

package com.stackify.stacktrace;
// import statements
public class StackWalkingExampleTest {
    // test methods go here
}

Stack Walking with No Options

Let’s start with a no-option StackWalker. This walker will walk through the call stack, retaining only frames of its interest and returning them as a list:

public List<StackFrame> walkWithNoOptions() {
    StackWalker walker = StackWalker.getInstance();
    return walker.walk(s -> s.filter(f -> f.getClassName().startsWith("com.stackify")).collect(Collectors.toList()));
}

The returned list consists of frames corresponding to methods whose class has a qualified name starting with com.stackify. This list has two elements, one denotes the method under test, and the other indicates the test method itself.

Here’s a test verifying that:

@Test
public void whenWalkWithNoOptions_thenFramesAreReturned() {
    List<StackFrame> frames = new StackWalkingExample().walkWithNoOptions();
    assertEquals(2, frames.size());
}

You can also go through the stack and perform a given action on each frame using the forEach method. You cannot filter or limit the number of extracted frames with this method, though.

The following method returns a list of all the frames captured in a stack:

public List<StackFrame> forEachWithNoOptions() {
    List<StackFrame> frames = new ArrayList<>();
    StackWalker walker = StackWalker.getInstance(Collections.emptySet());
    walker.forEach(frames::add);
    return frames;
}

The empty Set argument to the getInstance method is used just to make it clear that you can pass a set of options when creating a StackWalker. It doesn’t have any other meaning here.

This test checks the state of the returned frames:

@Test
public void whenForEachWithNoOptions_thenFramesAreReturned() {
    List<StackFrame> frames = new StackWalkingExample().forEachWithNoOptions();
    StackFrame topFrame = frames.get(0);
    assertEquals("com.stackify.stacktrace.StackWalkingExample", topFrame.getClassName());
    assertEquals("forEachWithNoOptions", topFrame.getMethodName());
    assertEquals(0, frames.stream().filter(f -> f.getClassName().equals("java.lang.reflect.Method")).count());
}

Notice the last assertion, which confirms that the stack walk didn’t keep reflection frames. You must specify an appropriate option to make those frames show up.

Using the RETAIN_CLASS_REFERENCE Option

Let’s now have a look at a StackWalker with the RETAIN_CLASS_REFERENCE option:

public StackFrame walkWithRetainClassReference() {
    StackWalker walker = StackWalker.getInstance(RETAIN_CLASS_REFERENCE);
    return walker.walk(s -> s.findFirst().get());
}

The walk method, in this case, returns the top frame of the stack. This frame represents the method calling the walk method itself.

Let’s create a simple test to confirm that:

@Test
public void whenWalkWithRetainClassReference_thenAFrameIsReturned() {
    StackFrame topFrame = new StackWalkingExample().walkWithRetainClassReference();
    assertEquals(StackWalkingExample.class, topFrame.getDeclaringClass());
    assertEquals("walkWithRetainClassReference", topFrame.getMethodName());
}

The getDeclaringClass method works due to the setting of the RETAIN_CLASS_REFERENCE option.

Using the SHOW_REFLECT_FRAMES Option

Next, let’s look at a method that configures a StackWalker with the SHOW_REFLECT_FRAMES option:

public List<StackFrame> walkWithShowReflectFrames() {
    StackWalker walker = StackWalker.getInstance(SHOW_REFLECT_FRAMES);
    return walker.walk(s -> s.collect(Collectors.toList()));
}

Here’s a quick test which verifies the existence of reflection frames in the stack trace:

@Test
public void whenWalkWithShowReflectFrames_thenFramesAreReturned() {
    List<StackFrame> frames = new StackWalkingExample().walkWithShowReflectFrames();
    assertNotEquals(0, frames.stream().filter(f -> f.getClassName().equals("java.lang.reflect.Method")).count());
}

The last option, SHOW_HIDDEN_FRAMES, can be used to show all hidden frames, including reflection frames. For instance, lambda expressions only show up in the stack trace when applying this option.

Summary

Java gives us many interesting ways to get access to a stack trace; and, starting with Java 9, the natural option is the Stack Walking API.

This is, simply put, significantly more powerful than the older APIs and can lead to highly useful debugging tools, allowing you to capture the call stack at any particular point in time, and get to the root of any problem quickly.

With APM, server health metrics, and error log integration, improve the performance of your Java apps with Stackify Retrace.  Try your free two week trial today

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]