top of page
Lokajit Tikayatray

8 Must-Know Debugging Techniques for Every Programmer


A vector art of a developer trying to debug

Debugging is more of an art than a science. It’s an experimental process that involves deciphering, diagnosing, and talking to the code.


While it may seem like the primary goal is to fix a bug, it’s actually a journey through the mind of the developer who wrote the code (and it can very well be you if you’re the author).


For example, if you come across a line of code that doesn’t make sense at first, you might ask, “What was the developer thinking?” This question is the essence of debugging. It’s about understanding logic, problem-solving, and communication.


A significant part of my success in writing good code is due to two primary functions that I do religiously.

  • One — test the code locally from all possible angles. Instead of proving that the code works, I try to see if it can break.

  • Second — knowing all the possible ways to debug an issue.

For the second part, let’s go through a list of effective debugging techniques that can help you become a better programmer.


The list is not a definite sequence in which you need to approach debugging. Neither one of the techniques is superior to the other. You must use all these methods as required to be a pro debugger.


Learning these debugging techniques will enhance your development skills.




1. The Art of Replication


Once a defect is assigned, the very first thing the developer should do is replicate it. Only attempt to put a fix once you can reproduce the error consistently.


Otherwise, all you’ll do is add a patch based on your assumption, which almost always will not fix the issue but may create a new problem.


Every time I get a defect, I replicate it in the environment where the issue is reported. I follow the description step by step and verify that it is a bug, not a testing error.


Next, I replicate the issue in my local environment by setting up the exact conditions under which the error occurs. I wait to write any code until it is reproducible locally.


Once I change the code, I test it again in my local. This way, I can guarantee my code is the correct fix before deploying it into the test environment.


Don’t skip the replication step even if you’re in a hurry to provide a resolution. Replicating the issue will help you resolve the problem faster as you avoid rework due to wrong assumptions.


2. Debugging by Subtraction


Debugging by subtraction is the approach of simplifying the code to isolate the problem. This approach involves progressively removing parts of your code until the error disappears.


The last bit of code removed likely contains the bug. It’s a process of elimination that helps narrow down the place of error in the code.


When it comes to debugging, simplicity is often the key to untangling the mess created by complex bugs. In ‘Debugging by Subtraction’, the idea is not about writing code but deliberately subtracting it.


The first step in this approach is to make a backup of your existing code to safeguard your work. Then, take parts of your code out of the equation systematically.


Keep testing at each step. When the bug finally disappears, you’ll know the last piece of code you removed was likely the culprit.


This method might look like an act of desperation. But in reality, it’s more of a strategic game of elimination. It can save you from chasing phantoms in vast codebases and help you zero in on the real issue.


I use this method only when other debugging techniques don’t seem to point me in the right direction.


This method needs a lot of patience as you have to remove a few lines, build the project, start the server, and test it to see if you can pinpoint the offending code. You need to repeat the cycle until you find the root cause.




3. Employing the Binary Search


Troubleshooting a pesky bug sometimes requires you to think more like a problem solver and less like a coder. That is where the binary search technique helps narrow down the bug’s source.


Based on the principles of the binary search algorithm, this technique is about dividing and working on each subset until you find the problem’s root.


Start by splitting your code into two halves. Test both separately and determine which half might carry the bug. Pick the half that reproduces the error, split them further into two halves, test them each, and try to see which one is the problem code.


Continue this process until you’ve pinpointed the exact line of code causing the issue.


This process of elimination offers a more strategic approach to debugging. It can significantly expedite your bug-hunting process, especially in larger codebases.


4. Rubber Duck Debugging


The rubber duck debugging technique involves explaining your code to an inanimate object. While bug hunting, sometimes an inanimate companion can be your best aid. This may sound odd, but that’s precisely what the “Rubber Duck Debugging” technique is all about.


rubber duck with goggles

Named after a practice where developers would detail their code to a rubber duck, this method underscores the power of externalization.


To use this technique, pick a rubber duck and start explaining your code piece by piece in a way that even a duck can understand.


This process forces you to slow down, rethink your logic, and articulate it in plain terms. Often, verbalizing the code in the simplest possible way reveals the logical flaws that were previously overlooked.


Conversing with an inanimate object about your code might feel peculiar, but it works wonders in bringing overlooked issues to the surface.


I often use this method when I am deep into my code, unable to figure out the root cause of a persistent bug. I usually don’t use a rubber duck per se. If you feel awkward talking to a physical inanimate object, imagine it and use the method.




5. Harnessing the Power of Print Statements


Often, we overlook the simple tools in our arsenal while seeking complex solutions. One such underappreciated tool in debugging is the humble ‘print’ statement.


It might seem archaic in the age of advanced debugging tools, but its power cannot be underestimated.


Imagine you’re going on a hike, but you’ve lost your trail. You don’t know where you’re, nor can you make out how you got there. A print statement, in the wilderness of code, acts like the breadcrumb trail that can lead you back to the path you took and the places you went wrong.


To implement this, strategically place print statements at various points in your code and monitor the state of variables or outputs.


As you run the program, these ‘breadcrumbs’ will shed light on the data flow and help you spot any anomalies or irregularities.


While simple, the ‘print’ statement is a surprisingly powerful tool. It provides an immediate window into the inner workings of your code, highlighting the problems. It’s a tool as simple as talking to a rubber duck, yet impactful in spotting the issues you might overlook otherwise.


A note of caution - Please don’t leave the print statements in your code that gets deployed to production. Use it only in your local environment and remove them while pushing to your repository.




6. Mindful Logging


Logs are invaluable when it comes to tracing the source of an issue. They’re the best source of truth for troubleshooting, especially when you cannot reproduce a problem or are not allowed to use the environment (e.g., production).


I feel frustrated when there is no proper logging in a code base. I am sure every developer at least once wished they had added more logs to their code to make it easy during a debugging session.


Simultaneously, you don’t want to overload your system with too many logs.


A bunch of unnecessary logs can divert your attention and make it difficult to pinpoint the right ones. It also increases the data size your logging system needs to index, increasing your licensing cost for the software.


Log management tools like Splunk are powerful in managing logs but can be super pricey.


Well-placed and thoughtful log statements can reveal the state of the system at various points, showing not just where the program crashed but the path it took to get there. It also helps you keep your license cost manageable.


For example, instead of just logging an error message when an operation fails, consider logging the state of the variables involved in the process.


This will give you a clear snapshot of what led to the error.


At the same time, if you’ve log lines with no dynamic variable, you can safely remove them or put them in the ‘debug’ or another log- level that doesn’t get printed in your production environment.




7. The Importance of Deep Diving


Knowing the system as much as feasible is the best way to debug any issue. Often, developers jump right into fixing the code that is assigned to them. They operate on the assumption they have based on the defect description or the error code.


Instead, the developer should actually know how the code intends to meet the original requirement. This knowledge will help locate the right place to find and fix the root cause.


If you don’t know the actual intention behind the code, then there is a high probability you’ll patch the code, fixing the symptom. This approach most often creates problems in other places of your application.


To avoid such recurring defects and wasted time, I always prioritize understanding the functionality around the bug.


Knowing the broader context helps look at the offending code from a new perspective. Instead of putting a patch, the knowledge helps me correctly write the code, i.e., make the flow work (and not just fix the defect).


8. Backward Debugging


A person standing on his hands

If you cannot figure out the issue using any of the above techniques, then stand on your hands and read the code upside down. It’ll help you see the code from a different perspective.


I am just kidding. Please don’t do that. It won’t help. :)


However, you can do something similar, i.e., troubleshooting the defect by starting from the problem to trace ts origins. This is like debugging the issue backward.


Consider a scenario where your calculation results in an incorrect output. Instead of beginning from the input and tracing the flow forwards, with this method, you start from the wrong result and walk your way backward through your code.


As you trace the path from the output to the input, you examine the variables, scrutinize the calculations, and question each step’s logic. This often helps to discover where the transformation from the expected to the unexpected happened.


Using the backward debugging technique is like working a mystery novel in reverse, starting from the climax and uncovering the narrative threads that led there. It’s a counter-intuitive yet incredibly effective approach to pinpoint elusive bugs in your code.


Final Thoughts on the Must-Know Debugging Techniques


Patience is the key to debugging any issue. It might seem tedious sometimes, but the meticulousness of each technique makes the difference between finding the bug and merely stirring the code.


Each bug you encounter is a mystery, a puzzle waiting to be solved. It’s an opportunity to learn and grow as a developer.


So, the next time you encounter a bug, don’t just consider it an inconvenience. Consider it a chance to hone your debugging skills, practice these techniques, and become a better developer.


Have you used any of these techniques successfully in your development process? Do you have a unique way to debug the code?


Please feel free to share it with all of us here so that we can learn from your experience.




Subscribe to my free newsletter to get stories delivered directly to your mailbox.



A must-read success guide for junior developer to thrive in their career

Recent Posts

See All

Comments


Post: Blog2 Post
bottom of page