Debugging by the Seat of the Pants

Two people recently asked me what IDE or other tools I used to develop. My answer was that I only use a compiler, GNU make and a text editor. What seemed most surprising to these people was that I don't use a debugger. I never have. I don't even really know how to use one. This seems to really puzzle some people. I've written fairly complex software, including compilers, a 3D game engine and a type analysis. As every other programmer out there, I have to find and fix bugs on a daily basis. Most of these are trivial, but others complex and hard to find. In this post, I'll discuss some of the strategies I employ to find bugs without special-purpose debugging tools.

Debugging is very much detective work. Your program isn't behaving the way you expect. The goal is to find what the root cause of the problem might be, and the best way to go about this is by reasoning logically about the problem. One possible technique is to proceed by elimination, or, as Sherlock Holmes would say:

"When you have eliminated the impossible, whatever remains, however improbable, must be the truth".

That is, you can use your instinct, your hunches, to make an educated guess as to what parts of the program's code might be causing the problem to happen. Most of the bugs I encounter are simple, and my intuition usually leads me right to the cause. Some bugs are unfortunately trickier. One of the worst mistakes new programmers make when it comes to debugging is to assume that some part of their code is perfect and cannot possibly be the cause of a given bug. Wrongly eliminating a possible cause can cause one to search for hours in the wrong places without ever finding the said bug. Debugging can begin with a hunch, but you should never discount a possible cause until you have verified that the cause is actually elsewhere.

The question, then, is how to go about narrowing down the possible cause of a bug. When using a debugger, one can install breakpoints in a program to stop the program and step through its execution, examining local variables. I simply use print statements to accomplish the same thing. By placing print statements at various points, I can see where the program execution stops (in the case of a crash) and also print local variables and data structures. By iteratively inserting and removing print statements, using my intuition in the process, I can narrow down the probable cause of a bug very quickly. The key is to determine where the state of the program stops conforming to my expectations and becomes corrupted, progressively narrowing down which piece of code caused this to happen.

You might still think that not using a debugger is silly. Why not use the tool if it exists? For one, I believe I can find bugs just as fast using the techniques I developed over the years. There is also a case to be made that being able to debug without a debugger is actually very useful for someone who works in compilers, because often, there is no debugger available. I've had to debug x86 machine code generated by my own JIT compiler, and to use a debugger there, I would have had to write one. Instead, I implemented a system to print out numerical values, and this was enough to debug my system. Finally, debugging complex software sometimes requires special techniques that go much beyond what a debugger does.

I recently worked on a dataflow type analysis for JavaScript which involved two simultaneous, interacting fixed point computations and millions of dataflow facts being propagated in the system. Most of the bugs I had to find in this case weren't crashes, but type inference errors caused by complex interactions in multiple areas of the system. Sitting down and single-stepping my program in a debugger would not have revealed any useful information there. Instead, I had to write one-off, special-purpose pieces of code to detect inconsistencies as early as possible and allow me to get different views on the state of the system while it runs. Debugging complex systems can be difficult, and I often find that the best strategy is to work to prevent latent bugs using unit tests and assertions, but that's a story for another time.