Walking Through the Problem
You have a consistent repro, and a general idea of where in your software system the bug might be. However, you don’t know why on earth the repro steps cause the software to break the way it does. You’ve never seen the code before, and don’t have a ton of time to troubleshoot — you just need a quick description of the problem that you can hand off to the developer responsible for that code, when he gets back from his vacation in Tahiti. Is it time to pull out the debugger?
I would like to start this segment of the series by saying that I am not a fan of debuggers. They are very often overused, misused, and abused to troubleshoot code that really needs to be rewritten for clarity. Too often people use the debugger to get into a special case, and then add an extra branch of logic to handle that special case on the spot without thinking about whether or not that’s the proper fix. Almost as bad is encountering developers who chant the mantra “get it in the debugger” whenever an issue crops up. To be crystal clear: most bugs can be fixed without a debugger. Furthermore, many bugs cannot be solved (or solved easily) under a debugger.
The debugger can be a convenient and powerful tool. However, before you use it, you will absolutely need repro steps. To all the unseasoned devs in the crowd: resist the urge to develop in the debugger. You should be thinking your code through carefully in your mind before you write it, and as you fix the flaws. Try to reason out what’s happening with your own wits, instead of relying on the debugger to show you. If you’re really stuck and can’t figure it out, then use the debugger to show you what isn’t behaving the way you expect — but treat it like looking at the answers in the back of a text book. You won’t learn if you always peek, and it’s a lot slower in the long run; it also leads to bad coding habits, making life miserable for other devs maintaining that code. As you get better, you’ll need the debugger less and less, because you’ll have trained yourself to get it right the first time, and more readily spot mistakes when you (or others) make them.
When is a good time to use the debugger? It’s really handy if the dev who wrote a chunk of code is out, or is too busy to investigate a problem directly. One situation where you may correctly apply a debugger is when the error that occurs generates a stack trace or an unhandled exception dialog. Given a repro and a stack trace, you will be able to place a breakpoint where the exception occurs, start the application up under the debugger, and go through the repro steps. Alternately, if you get an unhandled exception dialog, you’ll be able to simply click “debug”, and the debugger will automatically start and use the stack trace information to take you to the line that threw the exception. In either case, the debugger will allow you to investigate the state of the variables inside the running program, and in many cases allow you to walk up and down the call stack, to examine the state of earlier calls. This is usually enough information to determine where the logic error is — either directly where the error (usually an exception) generating the call stack occurred, or some piece of code higher up the call stack that made attempted an invalid operation or sequence of operations.
Beware that when a call stack is generated, it can sometimes be misleading. This is especially true for multithreaded programs, and programs that inconsistently swallow exceptions (e.g., empty catch-all blocks). In these cases, an error condition may have originated in a part of the code that is in a completely different place then the end of the call stack might suggest — it might be up the call stack several layers, and deep down a completely different branch of code invoked by an earlier function call. Users of Microsoft’s Visual Studio can cause it to break whenever an exception is thrown (as opposed to thrown and uncaught). This can cause the debugger to be very noisy, but is extremely useful for detecting these kinds of “delayed” exception issues. However, you should take steps to fix the code that’s swallowing the error, and ensure that whoever wrote that code gets a talking to.
Even better than a stack trace are assertions. An assertion is a sanity check that, in almost all cases, is only active in the “debug” builds of code. This means that assertions won’t help you out in production scenarios, but they are still extremely valuable in a test environment. Unlike exceptions that lead to stack traces, an assertion will call into a piece of code that will record the error immediately, usually in the form of an assertion dialog box. This means that you will generally stand a better chance of detecting problems where they happen, instead of being subject to the whim of the calling code’s exception handling. Furthermore, the standard assertion dialog box provides you with the option of attaching the debugger immediately, as well as the option to ignore the assertion and continue executing. You can think of assertions as debugger-enabled logging (logging is next week’s topic).
While debuggers are good at allowing you to quickly see the internal state at an instant of time, and as time progresses, they do have limitations. Threading issues — race conditions, concurrent access/trashing, deadlocks, and more — are often very difficult, if not impossible, to solve under the debugger. Resource leaks are also not easy to track down in the debugger directly, as is access to uninitialized or freed memory. These days, you’d be hard pressed to find a language that does not have a debugger — even Perl, reguler expressions, and Bash have debuggers. Keep in mind, though, that these can be awkward (PHPs XDebug), limited to a certain environment (JavaScript usually has one or more per browser/host), or not available on your OS (Visual Studio, or GDB). The debugging capabilities that are available for the language you are working in might not be enough to track down broader categories of bugs, so it is important that you not be 100% reliant on the debugger for your bug hunting needs.
The final thought I want to leave you with is that the debugger is a tool for fixing bad code. It is not an excuse to write it. Next week, we’ll go over logging.
Table of contents for Squashing Bugs
- Squashing Bugs: Intro and Repros
- What did you Do!?
- Walking Through the Problem
- Locating Bugs Through Records

