Debugging isn’t a science, it is an art (for the most part). Bugs in our code are pretty frustrating, but hunting them down and solving them is pretty satisfying. But whether you hate it or like it, it is a part of your life, and you have to come to terms with it. So to make life a little easier, here is a comprehensive list of things you can do to debug your code.
Before we begin, please note that these are tips that will work for any language that you choose to work with. That being said, it is easier to debug your code when using interpreted languages because hooks from them allow for simpler, interactive debugging.
Prevention is always better than cure
Before anything else, you need to follow this. Why deal with a bug when you can avoid it? It is likely that the error that will keep you up for 20 hours the next day is what you’re yourself putting in your code at the moment.
Debugging often begins as soon as code is created and continues in phases when code is coupled with other programming units to create a software product. Inadequately commenting on your code or not making it modular makes it harder for someone to debug your code when they need to. If a piece of code gets too long, break it into function calls that do a substantial amount of work. Doing so will not only increase legibility but also increase your chances of landing at the exact location of an error through function call traces.
Surprises can be good!
When a program fails to confirm its competence, it is a good surprise. It can lead to the discovery of a bug. Let’s be honest. Most of what we code through is based on our assumptions of how the thing is supposed to run. When it doesn’t, we know our assumptions weren’t accurate. And that is how we know where the bug could possibly be.
In the process of crossing out one of the many things we thought to be true, we learn something new. That is a concept derived from the software test called test to pass v/s test to fail — the latter results in a more foolproof software than the former.
Reproduce and reiterate
This is one golden tip you will find almost everywhere. Reproducing the bug often leads us to reiterate over the cause of it. This results in easy identification and removal. This necessarily does not uncover all of the bugs, but at least a few of them. If your code consists of a large loop, the most common bugs will probably come up in the first or second iteration. Also, minimizing the number of steps to reproduce the bug will eventually lead you to the approximate location as well.
Start small, do not over-complicate
Often, we tend to look at the error message and take our bug bounty session in a completely different path than it should have gone. Doing complicated repairs purely based on guesswork doesn’t really help. Obviously, we cannot always know the exact cause of an error and might have to rely on guesswork or assumptions, but narrowing down the reasons that an error could have arisen helps focus your search. Using a divide and conquer strategy reduces the time we spend looking for the bug, allowing us more time to fix it. Act as a binary search machine when chasing a bug.
Use an easy example initially, something that isn’t complicated and should work. Gradually narrow down the range by isolating the case you’re currently working on. Change just one thing at a time if needed. Changing too many things will decrease the accuracy of you hitting the target case. Clear the noise if there is any. This should help you focus on the issue and not other small discrepancies that might arise on the way.
There are many types of debugging. The primary ones include:
- Interactive debugging: using breakpoints, live debugger, etc.
- Print debugging: using print statements
- Remote debugging: debugging your program on a system different than what it is running on
- Post-mortem debugging: clearing up the mess after the program has crashed by inspecting various parts of it
Ditch print statements
For programmers who do not work on large-scale software or big pieces of code, using print debugging works almost every time. It isn’t, however, the best way to go about looking for faulty code chunks. The print debug method involves inserting print statements in your code at various places, so you know when the execution reaches that point. But more often than not, there are other things in the output area, and looking for your print statements among them is quite baffling.
Moreover, such an approach needs adding new trace code, re-running the program, again looking out for the output window for relevant details, and repeating the cycle all over. Apart from being time-consuming, it also diverts your attention from the task to the numerous repetitions. An even bigger mess is created when you finally have to clean your code, looking for all the places you put unnecessary print statements in.
Log the sequence
Logging events when executing your code is really useful. You typically need more than just the current state of the bug to figure out why something isn’t performing as planned. What’s the deal with a certain variable being null? A particular error message resulted in the system states changing in what way?
To answer these questions, you must first understand what occurred prior to the error. Logs show the events that led up to the current situation. Most IDEs these days come with in-built loggers. The best part about this is that logging, unlike a debugger, does not require absolute technical expertise with the tool being used. Logs are easy to read and mostly help you trace the bug much easier.
Though the above-mentioned tips should solve your bug, in a non-typical scenario, you might need to rely on language/IDE-specific debugging tools.
According to ScienceDirect: “A breakpoint is a location in the memory at which a program stops executing/pauses the execution and returns to the debugging tool or monitor program where you can change things in your program, to experiment with the effects of one part of the code on the bug at hand and go on to learn about part.”
Don’t throw away your debugging tool
No matter how much we, as programmers, abhor the process of debugging, sticking to conventional methods and not using pre-built tools that make our lives easier only adds to our frustration. Spending hours debugging something that a tool could have done in minutes makes no sense in terms of productivity. Moreover, when you’re working on large codebases, debugging complex systems becomes more difficult, especially when files and modules are strongly connected because changes in one might cause faults in another.
Debugging tools were made to make our lives easier and should be used for the same. They can also help us identify disastrous errors like segmentation faults and help identify where the function that caused the fault was called from. Monitoring variables automatically, some debuggers examine a test run to determine which lines of code were skipped. Simulators are provided by other debugging tools, allowing programmers to simulate how an app will appear and function on a certain OS or device.