Debugging Psychology, Strategy & Techniques— Lessons From “The Pragmatic Programmer” Part 2
By: Ashraf Guitouni
Tags: Pareto Principle, Software Development, Software Engineering, Strategy, Debugging

Photo by Zura Narimanishvili on Unsplash

This is the second part of our series: Lessons From The Pragmatic Programmer which we started by discussing disorder in software development and how to manage it. If you haven’t already, please consider reading the article here.

Introduction

We put a great deal of attention into implementing features correctly from the start, but writing flawless software is pretty much impossible. Many issues of all kinds will appear unexpectedly during the development lifecycle, sometimes requiring considerable time and effort to fix. Learning how to deal with these problems efficiently is a big part of our growth as software developers. But before getting into that, let’s run it back …

In 1947, a moth flew into a Mark II computer at Harvard University, causing a hardware failure. After a failed hardware test, it was caught and taped onto a paper report, labeled the “first actual case of bug being found”¹.

Snippet from “Log Book With Computer Bug” (National Museum of American History)

Since then, unexpected issues in computer systems are referred to as bugs.

What is Debugging?

Debugging refers to the practice of finding and understanding bugs with the intent of fixing them.

Everyone struggles with the debugging process to some capacity. This is because the mental approach and techniques it requires are difficult to master, and are often not properly taught (if at all) as part of software development training programs.

I hope that the ideas and tips shared in this article, make our debugging approach more pragmatic and, in consequence, more efficient.

Debugging Psychology

Bugs in our systems mean that we’ve committed mistakes during the development or integration of some features. Being faced with this reality through an ugly crash log, a tester’s report or a client’s negative feedback all invoke negative feelings.

To develop a stronger and healthier mindset towards debugging, let’s go over a couple of points.

1) Dealing with Feelings of Shame and Blame:

A filthy bug has revealed itself. One of these two thoughts may pop into our heads: “Who’s the nasty fellow behind this crime?” or “If this my doing, I’m toast!”.

Having these feelings of disappointment or denial is not the end of the world, but it sure won’t get us any close to solving the issue.

TIP: Fix the Problem, Not the Blame.

Debugging is not a criminal investigation, it’s problem-solving. Whether or not the bug is our fault, it becomes everybody’s problem as soon as it’s discovered, and fixing it requires our full concentration and collaboration. It’s also important to remember that finding an issue in our code does not diminish its quality or discredit the effort put into writing it.

2) Controlling the Panic:

When it’s debugging time, it’s easy to get into a panic, especially while under pressure. Needless to say, this can stand in the way of finding a solution. The tip, here, is really simple.

TIP: Don’t Panic.

Well, “Easier said than done”, right? But, just remembering this can help us take a step back, slow down our thoughts, then calmly and methodically search for the error. Controlling the panic could be the key to resolving the situation in the shortest time. As they say: “Go slow to go fast”.

Debugging Strategy

In addition to steering our thoughts and focus towards objective and productive directions, it helps to follow certain strategies while tackling the debugging task.

1) The Elimination Process:

Oftentimes, our systems are composed not only of code written by our team but third-party libraries and products. All these different components are, naturally, debugging targets.

Unfortunately, the search may not get us anywhere after a while, or we may be extremely confident in our code and can’t bring ourselves to doubt it. The book’s authors share a story that’s very relevant for such situations.

A senior engineer was dealing with a bug in one of his projects, so he claimed that the select system call was broken on the Unix system they were using. His peers tried their hardest to convince him that the system call can’t be the root of the issue, but he firmly stood his ground.

After weeks of failed attempts at implementing workarounds (that are based on this claim), the engineer was forced to take a step back and just review the documentation of select . It was, then, only a matter of minutes before he understood and corrected the problem.

TIP: ‘select’ Isn’t Broken.

Our code or process being broken is significantly more likely than the language being flawed, the library being broken, or the hardware being defective.

Before doubting external dependencies, it’s much more profitable to intensely debug our own code before anything else. Debugging can, thus, be approached as a process of elimination. Once we’re very sure that our code isn’t faulty, we can start looking at the library and beyond.

2) Using The Time Machine:

While searching for a bug, we tend to just move around the codebase in space, looking at different modules, functions, that is.

But when inspecting the solution at its current state doesn’t reveal the root of the problem, it’s a good strategy to move around the codebase in time, checking earlier versions to see if the issue can be reproduced there. This can be thought of as an extension of the process by elimination (described in the previous sub-section) to the time dimension.

In a lot of cases, checking a couple of the most recent previous versions can be enough, but in other cases, the issue could be caused by changes in much older versions. And so to avoid searching for the issue linearly, source code control systems support dichotomic search across the tracked versions. Git, for instance, provides the command git bisect for this purpose.

TIP: Always Use Source Code Control.

It’s never a bad time to share that tip …

3) Software Bugs and The Pareto Distribution:

The Pareto Principle², also known as the 80/20 rule is an aphorism stating that, for most events, roughly 80% of the consequences come from roughly 20% of the causes.

This principle is very prevalent in the software world, and understanding it can help us optimize our debugging process. Here are a few relevant 80/20 observations³:

80% of the complexity comes from 20% of the codebase (or features).

80% of the bugs come from 20% of the codebase.

80% of the development time is spent on 20% of the codebase (this can be thought of as a consequence of the first two).

The key takeaways from these observations are:

We can limit both the number and the severity of bugs by focusing on the most complex 20% of the codebase in terms of rigorous review and testing.

in terms of rigorous review and testing. When issues inevitably manifest in our systems, it pays off to focus on that 20% of the codebase while searching for the root of the problem.

Debugging Techniques

Now for the nitty-gritty of debugging, it comes down to using any type of method to validate our assumptions about the bug. These include:

Printf Debugging:

Also called “tracing”, it’s the technique of logging messages or expressions to an output. It’s simple to implement but it’s slow as it requires many iterations.

IDE Debugging:

It’s the use of debugging features provided by modern IDEs (e.g. Visual Studio, Eclipse, PyCharm). IDE debugging is centered around the use of breakpoints which are marked lines of code that, once reached, interrupt the program execution, and we can examine it at that state using different features such as:

Variable Inspection : by hovering over the identifier of any variable (including nested objects) in the editor, its value is revealed. Depending on the debugger and the programming language, extra information is often displayed as well (e.g. registers, call stack, i/o).

: by hovering over the identifier of any variable (including nested objects) in the editor, its value is revealed. Depending on the debugger and the programming language, extra information is often displayed as well (e.g. registers, call stack, i/o). Expression Evaluation: to check the value of something that’s not directly identifiable in the code (as a variable), it’s possible to evaluate expressions and run statements at the current state of the program using a debug console.

IDE Debugging with Visual Studio Code Example

Both methods (which can be seen in the example screenshot above) provide significant flexibility and time gains compared to printf debugging, but they‘re difficult to carry out in all environments (e.g. production).

Rubber Duck Debugging:

The story behind this, now very popular, technique is that David Thomas (one of the writers of “The Pragmatic Programmer”) noticed Greg Pugh, one of his very skilled coworkers, carrying around a rubber duck that would sit near his keyboard as he worked. After a few months, Dave had enough courage to ask.

Photo by S. Tsuchiya on Unsplash

The rubber duck was a substitute for another person to whom he would explain the code line by line while debugging. From experience, we all know that explaining something to someone else step by step not only helps us understand it better, but also helps us catch mistakes that we hadn’t noticed before. Because it’s hard to have another person accompany us during every debugging session, a cute little rubber duck can do.

Asking for Help

When we fail to find and solve a problem on our own, it’s time to ask for help. But, just like debugging, asking for help efficiently requires some strategy and technique.

When to Ask:

First and foremost, we should avoid requesting assistance hastily or tardily. On one hand, not spending enough time exploring the issue on our own means we can’t provide a lot of useful information when we ask people to assist us. On the other hand, waiting too long (without getting close to resolving the issue) before asking for help means the development process will be held up, or even worse, a live system will remain down for an extended period of time.

Whom to Ask:

Second, it’s important to develop a relatively wide support network that we can turn to for help. This network should include:

Members of our team: Naturally, they are the first target for our requests. Over time, interacting with our peers and learning about their skills can help us know who to ask in certain situations (especially in large teams). Tech Support: If our peers are unable to resolve the issue, we can look to submit a ticket to a support desk in case we’re using a supported product or a service. Online Communities: As a last option, it’s always possible to ask in online communities (e.g. Slack servers, Forum sites, IRC rooms), both general-purpose communities (e.g. StackExchange sites, Quora) and are specialized ones (which exist for virtually any technology).

How to ask:

Finally, knowing how to write the request and what information to provide is critical to getting help quickly and without troubling others too much.

The question should be framed politely and carefully, and should contain the following items if applicable:

Descriptive Title: The title of the post or email provides a very short description of the problem. It should be enough for someone to know whether or not they can help us. Problem Summary: Placed at the start of the message body, the problem summary develops what we mentioned in the title. It should highlight the expected behavior versus the actual (undesired) behavior of the system. Error Logs: Well-formatted text and not screenshots (which force people to type the potentially long messages in their search bar). Environment Details: Operating system, language, framework, and library versions. Reproduction Steps and a Minimal, Reproducible Example: A list of actions (usually command-line instructions) that can reproduce the issue as well as some code that other people can use to easily reproduce the issue, if possible. Creating a minimal, reproducible example⁴ may require a lot of time and effort, but it makes it easier for other people to understand the problem and provide a solution much faster than they would have otherwise (if at all). Attempts to Diagnose or Fix: The actions that we already tried to solve the problem.

Summary

Debugging is a big part of our line of work as software developers. Cultivating a positive mindset towards it as well as utilizing pragmatic strategies and proven techniques to approach it are key.

The ability to find and resolve bugs efficiently relies not only on our mental and technical abilities but also on the quality of our communication with our teammates, partners, and fellow software community members.

Links

[1] National Museum of American History — Log Book With Computer Bug

https://americanhistory.si.edu/collections/search/object/nmah_334663

[2] Investopedia — The 80–20 Rule

https://www.investopedia.com/terms/1/80-20-rule.asp

[3] Project Ricochet — The Pareto Principle, and the 80% solution

https://projectricochet.com/blog/software-development-pareto-principle-and-80-solution

[4] StackOverflow — How to create a Minimal, Reproducible Example

https://stackoverflow.com/help/minimal-reproducible-example