[Workshop Alert] Dynamic Scoring for WAF Actions and CloudFront Traffic - Save Your Seat Now!

Java Debugging: Using Tracing To Debug Applications

  • Lipsa Das
  • July 7, 2022
Share article

Write enough programs, and you’ll agree that it’s impossible to write an exception-free program, at least in the first go. Java debugging is a major part of the coding process, and knowing how to debug your code efficiently can make or break your day. And in Java applications, understanding and leveraging stack traces can be the game-changer you need to ship your application quickly.

This article will cover how to debug in Java and how Java stack traces simplify it. Let’s start with what debugging is.

What is debugging in Java?

Debugging in Java is testing your code under different scenarios, edge cases, and fixing any errors you might encounter. Depending on the complexity of the program, debugging can range from relying on the compiler to catch simple syntax errors or using a variety of Java tools such as debuggers and breakpoints to find logical errors that are easily overlooked. 

One tool that significantly speeds up the Java debugging process is called a stack trace. Not only does it help you submit relatively clean code during development, but it’s also one of the primary tools to troubleshoot your code after deployment. 

What is a Java stack trace?

A Java stack trace is the long list of function calls printed on the programming terminal in case of any errors in the program. Let’s understand how it is generated.

Java programs use a call stack model for execution. Each call stack is made up of stack frames that store function calls. That means anytime you call a method, the call stack creates a stack frame that stores information such as variables, arguments, return address, etc. If a program cannot execute successfully, the compiler prints a copy of the active stack frames to the terminal. That’s the stack trace.

As a stack trace lists all your function calls before the program crashes, it lets you know where you should start looking for the error. This is vital because it has the potential to cut down your java debugging time in half since you’re no longer flying blind.

Java Stack Trace Example

Let’s take the example of a program that takes three numbers and divides the largest by one of the other numbers and returns the quotient. 

public class GreatestNumber {
    
    //method which returns the quotient
  public static int greater(int a, int b, int c) {
      int div;
      if (a>b && a>c)
      {
          div = a / b;
          return div;
          
      }
      else if (b>c && b>a)
      {
          div = b / c;
          return div;
      }
      else
      {
          div = c/a;
          return c;
      }
  }
  
  
 //main method
  public static void main(String[] args) {
      int m;
      m = greater (0, 3, 10);
      System.out.println (m); 
  }
}

This program would work perfectly when all numbers are non-zero integers. However, since we have set the input for “a” as zero, the program triggers an Arithmetic exception.

Here’s the stack trace:

Exception in thread "main" java. lang.ArithmeticException: / by zero           
at GreatestNumber.greater(GreatestNumber.java:18)                              
at GreatestNumber.main(GreatestNumber.java:27)

How to read a Java stack trace

Sometimes, the sheer number of lines in a stack trace might feel intimidating. However, a big stack trace can also contain useful information that can help you debug faster. This section will help you understand the strategy to read even the most nested exceptions.

Let’s break down the example from earlier:

Exception in thread "main" java.lang.ArithmeticException: / by zero 

In the first line, the program tells us that the exception has occurred in the “main” thread, which is useful when there are multiple threads in your application. The next word tells us the type of Exception that has occurred — in this case, an ArithmeticException. Typically, every Exception will have associated documentation or “Javadocs” which you can refer to troubleshoot that error. 

The rest of the stack trace tells us where the exception occurred exactly. Traces are printed in a “last in, first out” manner. That means the last function called when the exception occurred will be printed first. Here, since the Arithmetic exception occurred while the program’s control was passed to the “greater” function, we can see that function call in the stack trace along with the line number. Now, where was this called from? It’s printed on the next line — the “main” method.

GreatestNumber.greater(GreatestNumber.java:18)at 
GreatestNumber.main(GreatestNumber.java:27)

Using this logic, you can decode even nested exceptions like these:

Exception in thread "main" java.lang.IllegalStateException: A phone has a null property
        at com.test.project.Phone.getModelNumber(Phone.java:28)
        at com.test.project.Bootstrap.main(Bootstrap.java:4)
Caused by: java.lang.NullPointerException
        at com.test.project.Phone.getModelNumber(Phone.java:12)
        at com.test.project.Manufacturer.getPhoneModel(Manufacturer.java:15)
        ... 1 more,

In this case, you want to traverse the “Caused by” sections until you find the section that might be the root cause of the error. Once you’re sure of that, you can follow the “at” trail to determine the exact line that needs fixing.

With this exception, the root cause is the “NullPointerException.” Thus, you should look at line 12 of Phone.java to figure out why the function returns a null value.

Handling exceptions in Java

Exception handling ensures that your application will still work even if one function doesn’t. You want your application to maintain (or at least attempt) a normal execution flow until you can fix the exceptions. A simple try-catch block or a try-catch-finally block usually does the job.

This is how it would look in our previous example if we include a try-catch block to address the ArithmeticException:

public static void main(String[] args) {
      int m;
      try {
          m = greater (0, 3, 10);
          System.out.println (m); 
      } catch(ArithmeticException e) {
        System.out.println("Division by zero is not allowed:"+ e.getMessage());
        e.printStackTrace();
      }
}

And the corresponding trace would be:

Division by zero is not allowed - Error message: / by zero
java.lang.ArithmeticException: / by zero
at GreatestNumber.greater(GreatestNumber.java:18)
	at GreatestNumber.main(GreatestNumber.java:28)

As you can see, the custom message makes it much clearer for users. They do not have to understand what an Arithmetic Exception is – only that zero is not allowed as an input parameter. Additionally, it provides more context to the stack trace, which is helpful when it is too long.

Exception Handling & Third-Party Libraries

Exception handling is essential when calling third-party code libraries. Since well-known Java code libraries are extensively tested based on secure code practices, exceptions typically occur due to internal code.

For instance, say, we’re trying to write to a file by using the IO library provided by Java. If the file does not have the associated permission, the library will throw an AccessControl exception that looks something like this:

java.security.AccessControlException: access denied 
("java.io.FilePermission" "test.txt" "read")
    at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
    at java.base/java.security.AccessController.checkPermission(AccessController.java:897)
    at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:322)
    at java.base/java.lang.SecurityManager.checkRead(SecurityManager.java:883)
    at 
java.base/java.io.FileInputStream.<init>(FileInputStream.java:225)
    at 
java.base/java.io.FileInputStream.<init>(FileInputStream.java:187)
    at java.base/java.io.FileReader.<init>(FileReader.java:96)
    at test.main(File.java:19)

In this example, the stack trace prints all the function calls inside three Java libraries — security, lang, and IO. This makes the stack trace confusing because the problematic code is not due to the library but instead due to how you use it. Thus, making sense of the stack trace in the Java debugging process and identifying references to the code or package you used becomes difficult. 

Instead, if you incorporate a try-catch block in your code, the stack trace would directly start from your code without traversing the internal third-party libraries. That’ll make the trace more readable and easier to handle.

Now that you understand how to read a stack trace and handle exceptions, let’s talk about managing them during Java debugging effectively.

Java tracing and writing them to logs

In distributed or complex applications, the number of function calls is massive. As you can imagine, errors cause lengthy stack traces, which can be hard to keep track of. This is even more problematic in large production-level events where multiple applications depend on each other. The solution is to write traces to logs and use a log management platform to manage them.

Some log management platforms, such as Coralogix, offer features such as log clustering, auto-parsing, and proactive anomaly detection. These features help you constantly monitor the health of your application and detect errors even before they occur. 

Investigating errors with Java debugger

Sometimes, finding errors is not as simple as a NullPointerException or an ArithmeticException. Nuances in the code can cause logical errors that can give incorrect outputs. 

In cases such as these, printing a stack trace using a Thread.dumpStack() is a good way to point your efforts in the right direction. You can choose strategic breakpoints based on the information provided by the trace and then go through the code line by line with a debugger.

With debuggers, you can halt the program’s execution, figure out the state of the variables and understand the program flow easily. Typically, Java IDEs like Eclipse have built-in debuggers that you use to carry out this activity.

The Ultimate Java Debugging Tools: Stack Traces & Log Management

Deploying code to production can sometimes be nerve-wracking. The actual problem starts when the code you deployed breaks already working applications. Since you don’t have the luxury of using debuggers in production, the only thing that can help you is a java stack trace and accessing those details to log files. Those log files make pinpointing the issue much easier and save you big escalations and headaches. You can also set up log monitoring platforms like Coralogix to constantly analyze your logs and set up alerts whenever an exception occurs.

Observability and Security
that Scale with You.