Debugging 101 ๐ง
Essential steps that can help you to debug your application
Introduction
Whether you're developing a new feature or investigating the root cause of an existing bug in your application, knowing how to debug effectively is a vital skill for every Software Engineer.
This article will cover steps of the debugging process and includes tips to try the next time your program execution is interrupted by an exception.
Understand the logs
The first step to debugging effectively is to consider the state of the application at the time that the bug appears. If you have access to logs for the application, it should be the first place to look for clues on how to identify and fix the issue.
If you are working on a medium to large-sized application, it is likely that the application logs a considerable amount. In this case, you would want to apply filters based on the logging level (probably on ERROR and WARNING first), date-time and host. You can even apply filters based on the thread name to understand what the thread was doing before it faced the issue.
You analyze the logs and find a stack trace that could potentially identify the issue, that's already a partial win. The next step is to break down the stack trace in order to find the problematic code. Stack traces can get long and start to look intimidating. Taking the time to carefully understand the stack trace is an important step in debugging. The stack trace outlines the various classes and the methods that the application executed in order to reach the exception. A formal definition of the stack trace is that it is a representation of a call stack at a certain point in time. Reading the stack trace can help you to reproduce the issue on your local system where you will be able to use a debugger tool to find the root cause of the problem.
Below is an example of a simple stacktrace:
Exception in thread "main" java.lang.RuntimeException: This is a test exception at com.example.StackTraceExample.methodB(StackTraceExample.java:16) at com.example.StackTraceExample.methodA(StackTraceExample.java:11) at com.example.StackTraceExample.main(StackTraceExample.java:6)
Dissecting the stack trace above we know:
- Thread which ran into the exception: main
- Exception faced: RuntimeException
- Exception message: "This is a test exception"
The call stack tells us that the exception was thrown in the StackTraceExample class. The class lives within the com.example package. The first method called was the main method and on line 5 methodA is called. After, on line 11 of methodA, the application calls methodB. The exception is then thrown at line 16 of methodB.
Reproducing the bug
After analyzing the logs and stack trace, you may want to reproduce the bug in your local environment. The benefit of reproducing the bug is that you will be able to test your fixes for that particular bug before deploying them to the testing or production environments.
If your application is logging appropriately, it should be logging the values of various variables at vital points of the application execution. For example, if you have an API that performs a complex calculation on values sent as input by the user and then returns the result, you would want to log the values of those inputs when the API is called. During the reproducing stage, you can then call the API with the same inputs.
Know your IDE
At this stage, it is helpful to know how to use your IDE (Integrated Development Environment).
By running the application in debugger mode during the reproducing stage, you can utilize debug points in order to see the state of various fields. Debugger mode allows you to see the values of all the fields in scope at various breakpoints that you set in your application. This is a more efficient approach than using print statements for debugging and the investment of getting familiar with the debugger is worth it in the long run.
You have successfully reproduced the bug, what is next? At this point of the debugging process, you would want to prepare a fix for the issue encountered.
For example, if you are using the IntelliJ IDE you can use the evaluate expression during debug mode to test potential fixes for the bug at any stage of the program execution using breakpoints and without the need to recompile the entire program with your potential fix.
Consider external factors
Not all bugs are created equal. When debugging an application, it is good to consider that the issue may be caused by an external factor and that your code is working fine ๐ค๐ผ
There are many external factors that can lead to problems during a program's execution. Examples of these external factors include:
- Issues with the data source (database or cache).
- Network slowness. If a significant activity such as a database replication is happening on the same network as your application it can lead to time-out issues.
- A problem with an external API that is being used. If you are retrieving data from an external API, any issues with that application can cause issues during your program execution.
- A problem with an external service. If your application is utilizing a service in the cloud, any outages that are affecting that service can lead to issues within your program.
If all else fails...
Have you tried everything and are still unable to find the root cause?
Have a break. Have a Kit Kat.
Taking a break can sometimes be the most effective step in the debugging process!