In software development, bugs are an inevitable part of the process. They're not just obstacles; they're opportunities to deepen our understanding of our code. This article will guide you through the fundamentals of debugging, focusing on core principles rather than specific tactics.
Let's establish a mental model of how code operates. At its core, code is about transformationâit processes inputs to generate outputs.
Inputs and outputs in programming are diverse and can occur at various levels of a system. They're not limited to just what a user directly interacts with or what a code function explicitly receives and returns. Inputs can be anything that provides data to a part of the system, while outputs are any results or changes produced by the system.
For example:
- At the UI level: in an online ticket booking system, you enter your travel details (input) to the browser form, and the system returns a confirmation and ticket (output).
- In a database context: an SQL query (input) retrieves user information (output).
- For a single function: parameters (input) are processed to return a value or create a side effect (output).
Each part of a system, from a single function to an entire application, can be viewed through this input-output lens.
The key detail is what happens between input and output. The data goes on a journey, being modified at various points based on rules specified by code (it could be your code or someone else's code referenced by your code). It's not magicâit's a series of deliberate transformations instructed by code.
So, what's a bug in this context? It's when the data takes an unexpected turn during its journey. Somewhere along the line, the data transforms in a way we didn't intend, leading to outputs we don't expect.
Understanding this flowâinput, data transformation, outputâgives you a powerful framework for debugging. When you debug, you're essentially retracing the data's steps, figuring out its intended path.
The debugging process
These are the steps to debug and resolve and issue:
- Reproduce the bug: Consistently create the error so that you know it exists. As time has likely passed since the bug was reported, it's important to ensure it still occurs. This also provides you with information about the inputs used to produce the bug.
- Locate and analyse the cause: Isolate where the problem data transformation is happening. You will want to observe the data as it gets transformed by code, probably with logs or a debugger.
- Fix the bug: Brainstorm various solutions to the bug and then implement the appropriate fix.
- Verify the fix: Try to reproduce the bug again (this is why step 1 is so important!). Also consider writing an automated test for the fix.
- Prevent recurrence of the bug: If the bug fix applied only fixed the problem in the short term, make sure to prevent the bug from resurfacing via automation or adjusting team processes.
The outlined steps above are foundational, but debugging is about more than just procedure; having the right mindset is crucial for efficient troubleshooting.
The debugging mindset
When debugging, you typically have some idea of how the system worksâor at leastâhow you expect the system to work. This mapâbuilt from your experience and assumptionsâis invaluable, but it can also lead you astray. It is why when debugging, it's imperative to prioritise evidence over intuition. When you run a test or add a log statement, trust what you see and not what you expect to see. If the output contradicts your assumptions, it's time to revisit those assumptions, not doubt the output (though there's nothing wrong with running the test again as a sanity check đ).
If your assumptions were all true, you'd probably know where the bug was already.
But what about when I'm facing a bug and I don't know WTF is going on?
We've covered the debugging basics, and they'll get you far, but sometimes, we encounter bugs that make us question our career choices and wonder if we should've become professional cake eaters instead.
We need more strategies.
The most challenging bugs fall into two categories: those that appear intermittently, and those beyond your current technical knowledge.
Bugs that appear intermittently are particularly tricky. They suggest we're missing something about what causes the problem. To solve these, we often need to think more broadly about what we consider as 'inputs' to causing the bug. For example, a bug might only appear during high network traffic, or when the system's memory is nearly full, or at specific times of day. These factors might not seem related at first, but actually are inputs that influence the outputs that cause the bug.
When you identify that the bug is happening because of how some code works that is beyond your current technical knowledge, it can be intimidating to delve into how the offending code works, but that is usually the only way to understand the bug and fix it. Consider, for instance, a web developer encountering a performance issue stemming from inefficient database queries. The solution might require delving into the nuances of indexing strategies or query execution plans.
You can also try asking someone who understands debugging better than you. They'll help you solve it and teach you lots of things along the way. Pay attention to how they approach the issue. What do they look at first? Why do they choose to investigate one area over another? How do they use the information they have to decide what to do next? You get the added bonus of seeing if they use any tools or tactics that you aren't using to improve your debugging!
Conclusion
We discussed a mental model of thinking about code as a series of data transformations from input to output that informs how to think about code (and bugs). We explored the key steps of debugging, delved into the mindset of effective problem solving, and tackled strategies for the most challenging bugs.
I've given you the fly swat, now go smack those bugs (đ)!