Every software developer faces bugs and errors in their source code at some point. Whether it is while developing the code or after deploying the application in the test environment, bugs are the bane of a developer’s existence. In these cases, developers use different tools and techniques to identify the bug or error in the source code and resolve it.
Introduction to Debugging
Debugging is the technique of monitoring a software code and finding and removing any potential errors, bugs, or defects that can cause the application to behave unexpectedly or crash. Debugging plays a very crucial role in the software development life cycle.
Debugging involves multiple steps, like identifying the source of the bug, correcting the problem, or fixing the bug. This sounds simple enough. However, debugging really becomes a nightmare when the various subsystems or modules are tightly coupled. Sometimes, it takes more time to debug or fix bugs than it takes to do the actual coding. To simplify the process, developers can locate the bug and remove them using various tools and techniques.
To resolve the defect in the program or application, the developer has to identify the piece of source code that is causing the problem. Various debugging tools can be used depending on the stage of development. Developers can trace the program execution step by step by evaluating the values of the variables and the return values of the functions.
The types of errors and the debugging techniques vary depending on the stage of development and the environment.
Types of Possible Errors
To understand different types of debugging, we need to understand the different types of errors. Errors are usually of three types:
- Build and compile-time errors
- Runtime errors
- Logic errors
Build and compile-time errors happen at the development stage when the code is being built. These errors are thrown by the compiler or the interpreter while building the source code. In other words, build or compile-time errors prevent the application from even starting. These errors often result from syntax errors, like missing semicolons at the end of a statement or class not found. These errors are easy to spot and rectify because most IDE or compilers find them for you. The compiler or the interpreter will tell you the exact piece of code that is causing the problem.
Runtime errors occur and can be identified only while running the application. They occur only when the source code doesn’t have any compiler or syntax error, and the compiler or the interpreter cannot identify the runtime error during the build stage. Mostly, runtime errors depend on the user input or the environment. These kinds of errors can be identified by using try-catch blocks in your program and logging the error message properly.
Logic errors occur after the program is successfully compiled and running and it gives you an output. A logic error is when the result of the program is incorrect. These errors can not be caught using the try-catch blocks. Logic errors are also called semantic errors, and they occur due to some incorrect logic used by the developer to solve a problem while building the application.
Types of Debugging
Not all errors can be treated or debugged in the same way. The developer has to set up the right strategy to fix different errors. There are two types of debugging techniques: reactive debugging and preemptive debugging.
Most debugging is reactive—a defect is reported in the application or an error occurs, and the developer tries to find the root cause of the error to fix it. Solving build errors is easier, and generally, the compiler or build system clearly tells you the errors.
Solving a runtime error can be done using a debugger, which can provide additional information about the error and the stack trace of the error. It will tell you exactly the line where the fault is happening or exception is raised.
Solving logic errors is trickier. We don’t get clues as we do in other errors from the compiler or the stack trace. We need to use other strategies to identify the code causing the error.
Reactive Debugging
Reactive debugging refers to any debugging protocol that is employed after the bug manifests itself. Reactive debugging is deployed to reduce runtime and logic errors. Examples of reactive debugging are print debugging and using a debugger.
Print debugging
Print debugging is the print statements developers write in their source code (printf in C or System.Out.print in Java or print in Python). Developers write them to print out the values of the variables to the console or to the file to get a better sense of what is happening. These printed statements help find the root cause of the problem, especially when a variable has unexpected values.
Let’s say, for example, the source code has three functions, foo, bar, and baz. The source code is printing wrong values in baz, and baz is using foo and bar functions. To find the root cause, developers can add the print statement in the foo return values first, then bar, and then baz. The developer will identify the function causing the problem by narrowing down the search. Once we find the function returning the wrong value, we can analyze the variables or logic inside it.
Debugger
IntelliJ Debugger Attached to the Java Code
Print debugging is an easy technique, and sometimes developers use it to find the root cause of an error. However, it is a time-consuming and tedious process if the developer is dealing with several modules, functions, and variables. If a developer finds themself writing more than 10 print statements to identify the issue, then it’s time to switch to a debugger.
The debugger allows the developer to trace the execution of the program and identify the state of the variable functions as the program runs. In a sense, it is not too different from a print debugger. What makes it better for larger applications is that it prints every single variable and line of source code as the program executes. Integrated development environments like Eclipse, Jetbrains IntelliJ, Pycharm, and Visual Studio include built-in GUI debuggers. There are many command-line debuggers like GDB for C and pdb for Python available in the market.
Additionally, sometimes one can face a situation where the application in question runs on a different host. Collecting data from that system thus becomes really challenging. In such cases, remote debuggnging allows developers to trace the issue on a different host. Most modern IDEs support remote debugging. You can, at times, also couple remote debugging with multi-client debugging tools when the deployment architecture is multi-node or micro-service.
Preemptive Debugging
Preemptive debugging involves writing code that doesn’t impact the functionality of the program but helps developers, either catch bugs sooner or debug the source code easily when the bug occurs.
Assertions
Most programming languages have the inbuilt ability to add assertions in the source code. An assertion is a binary condition that is false. The assertions will make the execution of the code exit immediately with a helpful message.
Example:
assert value >= 0, “Cannot divide with a negative value”
Logging
Logging is not very different from print debugging. Logging statements of the variables or the error trace are added to the source code during the development. These logging statements will not affect the functionality of the application but writes the log statement to the file or console or database, etc.
Note, the logging statements are not added in reaction to the errors or bugs in the source code. They are added from the get-go because the information included in the log statements can be useful in identifying which functions are receiving the proper inputs and returning the expected result and which are not. This helps us streamline the search.
log.info(“persisting the item {} Quantity {}”.format(item, quantity))
Conclusion
Most debugging is like investigating a murder—you are both the detective and the murderer. Your takeaway from this guide should be the following:
- During the development, assume nothing, verify everything should be your rule of thumb. This will help you avoid errors in production.
- During debugging, narrow down your search as much as possible, use the proper tools to increase productivity, and treat warnings as seriously as you would compiler errors. While a warning will still allow your program to compile, it may be a sign of trouble further down the road.
Also published on Medium.