Java Debugging: Using Tracing To Debug Applications

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.

Five Tricks that Senior Engineers Use When They’re Debugging

Debugging is a fundamental skill in the arsenal of any engineer. Mistakes happen, and bugs are inevitable, but a skilled debugger can catch a bug early and find an elegant solution. 

But wait, what exactly is debugging?

It’s tempting to think of debugging as solving a problem. You’re fixing it, so it doesn’t come up again. This is not true. You’re not at the point of a solution. Right now, you’re investigating. You’re trying to work out the problem before committing to a solution. 

“Debugging is like being the detective in a crime movie where you are also the murderer.” – Filipe Fortes

So let’s get into it. Here are five debugging techniques that will supercharge your debugging skills and give you insight into even the most complex problems.

1. Use the IDE

A typical pattern that many engineers do is rely heavily on their minds. This pattern is good, but it’s not optimal. Your IDE comes packed with tools that will help you analyze a problem, like the software debugger tool that will help you to step through a coding problem, line by line. The important thing about the software debugger tool is that it can also tell you the value of every single variable, how many times a loop is iterated, whether an if-statement is entered, and more. When you’re debugging in programming, this tool is indispensable.

Remember though – the IDE won’t tell you the problem…

All the IDE can do is surface as much information as possible. It’s up to you to do two things. The first is to set up your IDE best for your working style. Some people like to have every window and dial open to see all the metrics as they work. Others prefer a clean, minimalistic working space. The second is to use the information put in front of you effectively. Your IDE gives you everything – it’s up to you to filter that down into insights. When you get the tooling right, it’s time to think more methodically about the problem in front of you.

2. Write out your assumptions

Debugging is fundamentally a testing activity. You’re poking your creation to truly understand how it behaves and, most importantly, why it behaves that way. When you’re testing, you have to make a series of assumptions. It’s a natural part of the testing process, but sometimes, those assumptions can be incorrect. 

Our assumptions can be so fundamental that we overlook them, but it’s valuable to be thoroughly conversant with the assumptions you’re making for complex issues. 

BDD is a great tool here

Behavior-driven development (BDD) is a powerful technique for surfacing assumptions, describing expected behavior, and flushing out any inconsistent thinking you have. It is most commonly utilized in the software design phase, but it’s equally valuable when trying to understand existing software. 

Try this workflow. 

  1. Write out a BDD case, for example, given that I have already logged in and my account has admin powers, when I navigate to the home page, I should be able to edit the page’s title. 
  2. Test it by navigating and trying it out. 
  3. You know your BDD test case is wrong if it doesn’t work. At that point, you can ask the following questions: Are you logged in? Does the account have admin powers? Do admins have the ability to edit pages?
  4. Create a new BDD case and start again, but this time more refined. Iterate towards the true issue, learning each time.

Mastering your use of language to surface your assumptions is one of the vital debugging techniques, but sometimes it’s not just about your language – it’s about your thinking. Sometimes you need someone, or namely something, to talk to. 

3. Rubber duck debugging

Sometimes, it’s a good idea to completely abandon your sanity and talk to inanimate objects when you’re stuck. This may sound a little wild, but it has a long and convincing pedigree in software engineering. The beauty of speaking to a rubber duck is that the conversation is agonizingly one-sided, forcing you to articulate every detail. 

Speaking about a problem often helps you find inconsistencies or assumptions in your thinking. These assumptions can lead to new test cases that will help you focus on the issue. There are dozens of senior engineers who swear by rubber duck debugging. 

People work too!

You could also have a conversation with a colleague. Asking for help is not a sign of weakness; it’s the sign of an efficient, collaborative engineer. If there is an available colleague nearby, have a chat! But sometimes, there’s nothing better than the vacant, plastic stare of a rubber duck.

Conversing is an essential method for understanding the problem, but sometimes the problem is so complicated that you need to take a different tactic. If the code is complex or difficult to understand, then it’s time to try and change the behavior to test your understanding.

4. Mess with the code a little

Okay, so you’ve tried a bunch of stuff, and it’s not working how you expect. You don’t know why and every input you’ve been attempting to give the code just isn’t making any sense. Your next step is to change the code a little. Here is what you do:

  1. Predict how the behavior will change if you change the code in a certain way.
  2. Change the code.
  3. Measure the outcome and see if your prediction is correct.
  4. Make a new prediction and start again.

This is a brilliant way of understanding why each function is in place for very complicated algorithms. Change a little, and investigate why it doesn’t match your expectations. Only now, you’re not investigating the whole algorithm; you’re just looking at one little step. When debugging in programming and the code is complex, making micro-changes and predicting their outcome is an excellent way of practically assessing the code’s behavior and can break up those long, monotonous staring competitions that engineers often find themselves in. 

However, if all else fails, there is one age-old debugging technique aiding engineers since we first started building things.

5. Get up and go outside

Newton didn’t discover gravity when an apple hit him on the head, but the myth has a sensible moral. Sometimes, ideas just hit you on the head, and no amount of brute force thinking will speed up that discovery. When you focus for a long time, the assumptions you make begin to pile up. It can become difficult to see through tunnel vision.

To get away from this, take yourself away from the problem for 10 minutes. Make a coffee, go for a walk, go to the gym – just do something that lets your mind work on the problem in the background. Your mind has a startling capacity for passive problem solving, and it is one of the most underutilized debugging skills.

Now you know what to do

Debugging can be one of the most rewarding engineering experiences, that warm sensation of enlightenment when you finally discover the actual workings of your solution is hard to beat. So the next time you run into a problem, don’t panic – just remember to be methodical, focused, and relaxed. You’ve got this.

Are your customers catching production problems 🔥 before you do?

Availability and quality are the biggest differentiators when people opt for a service or product today. You should be aware of the impact of your customers alerting you to your own problems, as well as how to stop this from becoming the norm. To make sure you don’t become an organization known for its bugs, understanding the organizational changes required to deliver a stable service is key. If, as Capers Jones tells us, only as many as 85% of bugs are caught pre-release, it’s important to differentiate yourself with the service you provide.

The Problem

It’s simple to understand why you don’t want unknown bugs to go out to your customers in a release. To truly understand its impact, you need to define the impact of committing problematic code to release, before we look at its solutions.

Problem 1: Your customers’ perception

No one wants to buy a faulty product. You open yourself up to reputational risks – poor reviews, client churn, lack of credibility – when you get a name for having buggy releases. This has three very tangible costs to your business. First, your customers will cease to use your product or service. Second, any new customers will become aware of your pitfalls sooner or later. Lastly, it can have a negative impact on staff morale and direction, and you’ll run the risk of losing your key people.

Problem 2: The road to recovery

Once a customer makes you aware of a bug, you don’t have a choice but to fix it (or you’ll be faced with the problem above). The cost of doing this post-production is only enhanced with the time it takes to detect the problem, or MTTD (mean time to detect). As part of the 2019 State of Devops Report, surveyed “Elite” performing businesses took on average one hour or under to deliver a service restoration or fix a bug, against up to one month for “Low” performing businesses in the same survey. The problem compounds with time: the longer it takes to detect the problem, the more time it takes for your developers to isolate, troubleshoot, fix and then patch. Of all surveyed in the 2019 State of Devops Report, the top performers were at least twice as likely to exceed their organizational SLAs for feature fixes.

Problem 3: Releasing known errors

Releases often go out to customers with “known errors” in them. These are errors that have been assessed to have little impact, or only occur under highly specific conditions, and therefore are unlikely to affect the general release. However, this is just the coding and troubleshooting you’ll have to do later down the line, because you wanted to make a release on time. This notion of technical debt isn’t something new, but with many tech companies doing many releases per day, the compounded work that goes into managing known errors is significant. 

The Solution

Organizations can easily deliver more stable releases to their customers. Analysis indicates that there are a number of things that can greatly enhance your own stability, and keep your overheads down.

Solution 1: What your team should be doing

Revisiting the 2019 State of Devops Report, we can see the growing importance of delivering fast fixes (reduced MTTD) is dependent on two important factors within your team.

Test automation is being viewed as a “golden record” when it comes to validating code for release. It positively impacts continuous integration and continuous deployment, and where deployment is automated, far greater efficiencies are achieved in the fast catching and remediation of bugs, before they hit the customers.

“With automated testing, developers gain confidence that a failure in a test suite denotes an actual failure just as much as a test suite passing successfully means it can be successfully deployed.”

Solution 2: Where you can look for help

The State of DevOps report also tells us that we can’t expect our developers to monitor their own code as well as use the outputs of infrastructure and code monitoring to make decisions. 

This is where Coralogix comes in. Coralogix’s advanced unified UI allows the pooling of log data from applications, infrastructure and networks in one simple view. Not only does this allow your developers to better understand the impact of releases on your system, as well as helping to spot bugs early on. Both of these are critical in reducing your RTTP, which leads to direct savings for your organization.

Coralogix also provides advanced solutions to flag “known errors”, so that if they do go out for release, they aren’t just consigned to a future fix pile. By stopping known errors from slipping through the cracks, you are actively minimizing your technical debt whilst increasing your dev team’s efficiency.

Lastly, Loggregation uses machine learning to benchmark your organization’s code’s performance, building an intelligent baseline that identifies errors and anomalies faster than anyone – even the most eagle-eyed of customers.