In this topic, you will get acquainted with an important feature that will help you with debugging your applications. It is called Stack Trace. It shows the call stack in an application up to the point where the stack trace message was generated. It appears as a message in your IDE when the application throws an error. We will analyze an example of such a situation and learn what a stack trace message can tell us and how to interpret it. You will also learn how to get a stack trace at any point of the program runtime when you need it.
As you may already know, the call stack is a LIFO data structure providing information about the execution order of methods. It is composed of stack frames. Each stack frame represents a single method.
Stack trace in detail
When you were learning about different types of exceptions, we also discussed different ways of throwing exceptions. Now, it is time to explore the messages behind them. Let's look at the following example:
import java.util.Scanner
fun main() {
val scanner = Scanner(System.`in`)
val input: String = scanner.nextLine()
val number = input.toInt() // an exception is possible here!
println(number + 1)
}If we enter a word instead of a number (for instance, "Kotlin"), the application throws an error and shows the following stack trace message:
Exception in thread "main" java.lang.NumberFormatException: For input string: "Kotlin"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:668)
at java.base/java.lang.Integer.parseInt(Integer.java:784)
at MainKt.main(Main.kt:6)
at MainKt.main(Main.kt)First, we need to read the top line, where we have three important hints:
The thread in which the exception was thrown. If you remember, when the application starts, it creates a main thread.
The class responsible for the type of error. In our case, it is the
NumberFormatExceptionclass from thejava.langpackage.A message indicating why the exception was thrown (here, entering the string "Kotlin"). Further on, you will see how this message was generated.
Now, let's move on and explore the remaining four lines. The second line from the bottom points at line 6, which is found in the main method. This is the line of the program whose execution led to the exception. The next invoked method was toInt().
public actual inline fun String.toInt(): Int = java.lang.Integer.parseInt(this)Inside this method, another overloaded method parseInt(s: String, radix: Int) from the Integer class was invoked on line 784.
public static int parseInt(String s) throws NumberFormatException {
return parseInt(s,10);
}The line numbers of base Java classes can vary depending on the Java version.
Inside the second parseInt(String s, int radix) method, on line 784, the application throws an exception invoking the NumberFormatException.forInputString(String s, int radix) method.
if (digit < 0 || result < multmin) {
throw NumberFormatException.forInputString(s, radix);
}Finally, in the fourth line from the bottom, we can see the invocation of the forInputString(s, radix) static method from the NumberFormatException class. Below, on line 64, you can see the message from the stack trace example above. That is how the message from the very first line was generated.
static NumberFormatException forInputString(String s, int radix) {
return new NumberFormatException("For input string: \"" + s + "\"" +
(radix == 10 ?
"" :
" under radix " + radix));
}Now, let's make some changes in our application. We are going to move part of the code to the method so that it will also be called when executing the application.
import java.util.*
fun main() {
val scanner = Scanner(System.`in`)
val input = scanner.nextLine()
demo(input)
}
fun demo(input: String) {
val number = input.toInt() // an exception is possible here!
println(number + 1)
}This time, we have one more line in our stack trace – it represents the execution of the demo(input: String) method.
Exception in thread "main" java.lang.NumberFormatException: For input string: "Kotlin"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:668)
at java.base/java.lang.Integer.parseInt(Integer.java:784)
at MainKt.demo(Main.kt:10)
at MainKt.main(Main.kt:6)
at MainKt.main(Main.kt)If you know what a call stack is, you might have already guessed that in this example, you have seen some call stack methods. Basically, this stack trace message shows the call stack from the main method up to the place where the exception was thrown.
The following diagram represents the call stack of the example above. Since the call stack is a LIFO data structure, the main() method that was called when the application was launched is at the bottom, and it will be the last printed element of the stack trace.
Getting a stack trace on demand
We have discussed an example of getting a stack trace after your application throws an error. What if you need to get a stack trace at some specific point? It can be obtained without throwing an error by calling the Thread.currentThread().stackTrace method. This way, it returns a StackTraceElement array, and you can print the stack trace using a loop.
for (element in Thread.currentThread().stackTrace) {
println(element)
}There are also other ways of getting a stack trace, such as calling the Throwable().stackTrace or Throwable().printStackTrace() methods. You can find them in the documentation and explore them on your own.
StackTraceElement class: an overview
As you may have noticed, the for loop example above prints an element of the StackTraceElement type. According to the official Java Documentation, the StackTraceElement class is described as an element in a stack trace representing a single stack frame. That is, each element returned by Thread.currentThread().getStackTrace() is a stack frame, where the element printed at the top represents the execution point where the stack trace was generated.
Now, let's launch the application and print a stack trace using the for loop we mentioned before.
import java.util.Scanner;
fun main() {
val scanner = Scanner(System.`in`)
val input = scanner.nextLine()
demo(input)
}
fun demo(input: String) {
for (element in Thread.currentThread().stackTrace) {
println(element)
}
val number = input.toInt() // an exception is possible here!
println(number + 1)
}If we input a number and the application does not throw an exception, the stack trace message will print the following three lines.
java.base/java.lang.Thread.getStackTrace(Thread.java:1610)
MainKt.demo(Main.kt:10)
MainKt.main(Main.kt:6)The most useful feature of the StackTraceElement class is that it provides methods to simplify these lines and get only the necessary information. If you print println(element.className) inside the mentioned loop, you will get a stack trace message in the following form:
java.lang.Thread
MainKt
MainKt
MainKtOther methods of the class, such as getMethodName() or getLineNumber(), work in a similar way. We will not discuss them in detail here, but we recommend that you study all the methods on your own.
Conclusion
In this topic, you've learned about stack trace, a Kotlin language feature that helps you understand the steps of program execution and, if necessary, quickly analyze why the application threw an error. At first glance, a stack trace message may seem complex and confusing, but once you get a solid grasp of it, it turns into a useful tool for debugging applications.