Strategies for Debugging That Lead To Self-Enlightenment
April 10, 2023
Welcome to a journey of self-enlightenment through the art of debugging. In the world of software development, debugging is often viewed as a necessary but tedious task to fix code bugs. However, what if debugging could be more than just a technical process? What if it could also be a profound practice that leads to self-awareness, mindfulness, and even self-enlightenment?
Our journey towards self-enlightenment starts with pain and suffering. Recently, I found myself immersed in a complex legacy Python Django application at work, facing unfamiliar code and web scraping for critical billing operations. While I’ve shared informal notes about debugging and git, as well as strategies for picking up new codebases, I came to the realization that I needed to transcend beyond mere debugging tricks. I needed a belief system, a guiding light towards self-enlightenment.
Building on the foundation of these documents as my canonical scriptures, I discovered three main pillars for debugging that led to self-enlightenment.
I. Write A Test
While doing debugging work, I sometimes find myself losing sight of the actual bug fix needed, wasting time coding up a refactor only to find that it doesn’t fix the actual bug. Sometimes I would even open a pull request without even validating that the bug has been fixed. Sometimes I create a bug fix, then clean up the mess I made from debugging, and in the process find that my fix has regressed, and once again reverted back to the original bug behavior. Gyeh.
I need to remind myself: it’s not fixed until it’s fixed.
As we all know, code is much better than humans when it comes to validating that something is fixed. In other words, it’s best to have a deterministic, continuous and quick way to programatically validate the bug fix.
This is why I’ve learned to write a test to validate the bug fix, before even starting to work on implementing a bug fix.
The test doesn’t have to literally be a unit test in your test framework. The key is to make the validation path automated, fast, and devoid of human judgement. This can simply be a curl request put into a
test-bug.sh file, or a simple node js script. If the bug is frontend related, the test could instead be a standalone page that runs some custom code to directly validate the bug on initial load.
The point here is that a test to reproduce your bug should be:
- Removing any manual clicking/typing steps to validate the behavior
- Repeatable infinitely
- Provide immediate feedback as to the status of the bug behavior
- Failing in the beginning of the debugging process
TLDR: progamatically validate the bug behavior with a script, custom code, or a unit test.
I’ve previously written about hunting bugs with
git bisect, and it turned out investing the time to understand how to use this git feature has proven to be an absolute boon in my quest for debugging salvation.
As I mentioned, I’ve been working in a complex legacy codebase built on technologies that are not my usual tech stack. Sometimes when looking for a bug, I just want to find the literal line of code that introduced the bug, without getting sidetracked by first trying to grok the internals of the Django web framework. By using
git bisect, I’m able to narrow down the location of the bug to a single commit. From there, I know I only need to grok the 200 or so lines from the commit, rather than taking shots in the dark or taking the time to understand the internals of the framework, or trying to decipher a minute detail from the height of the system as a whole.
Astute git users will notice how
git bisect works under-the-hood:
git is in fact using binary search to narrow down which commit introduced the bug. This is actually a great debugging strategy in general. We can extrapolate this idea outside of
git bisect and use it in other ways. On your journey to self-enlightment, you’ll find yourself bisecting pretty much everything.
For example, comment out half of a React component and replace it with a noop, and run your test. If the bug is still there, then you can confidently know that the bug is in the other half of the React component. You’ve just halved the surface area for which you need to debug. Repeat.
TLDR: Use binary search to recursively narrow down the problem area.
III. Backport a solution
On my journey towards self-enlightenment, I often felt overwhelmed with the shear size of the codebase I was working with. Even with the power of programmatic testing and bisecting, I was often bogged down by the many dependencies, multi-step processes to deploy a change, and multi-faceted nature of the application I was working on. Wouldn’t it be great if I had a miniature version of this application that I could test a bug fix on? Instead of spending all this time debugging in the context of this giant application?
This is where our third strategy for debugging that leads to self-enlightment comes into play: backporting a solution. This strategy utilizes reasoning by analogy.
Simply put, create a simplified version of the implementation with a simplified version of the bug in a “hello world”-esque codebase. From here, you can come up with a solution to the bug using the simplified codebase to move fast. After coming up with a bugfix, then apply the codefix to the large production codebase. This is called backporting — apply the solution back to the large codebase.
TLDR: Create a simple solution to the bug in isolation, then apply to the larger codebase.
So yeah, in conclusion, follow these three pillars for debugging, and you’ll be chanting:
Gate Gate Paragate Parasamgate Bodhi Svaha
This is a buddhist mantra which is chanted to signify the attainment of enlightenment and the transcendence of suffering. Good luck.
Key visual: Somewhere in Joe’s Valley, with a debug visual on top of it