Fixing bugs is a huge part of a developer’s job, but not many think or read about the actual process of solving a bug. We sort of go at it intuitively, trusting our own self-developed process.
Some of us tend to think of bug solving as the 2nd class citizen in programming. Not our real job, which is writing code, crafting new features and discovering new technologies. It’s just something we do in between real programming, right?
I think we should give bug solving it’s rightful and respectful place in the profession as a first-class citizen. Bug solving can be a lot of fun and very educational. In bug crunching times, I make it my own personal daily challenge to crack as many bugs as possible, turning it into a game of sorts.
Here are some tips and strategies for bug-solving mastery.
1. The Art of Reproducing
Fixing a bug should automatically start with reproducing it. This serves several important purposes. We can see the bug still reproduces, and that it reproduces the same way as in the bug description. Sometimes we’ll see the bug was already fixed by another developer, and we can save some time. Finally, reproducing means we can verify the bug is actually solved after our applying the fix.
Don’t fix bugs blindly in code without actually experiencing the problem. I’ve witnessed too many times developers fixing bugs blindly, without initially reproducing and without verifying the bug was fixed. This almost always ends badly as the bug ends up unsolved. It creates much more work and creates a lot of tension between QA and development.
Anyone who worked in software long enough, will testify that reproducing a bug is often half the battle. The most difficult and frustrating bugs are the ones that don’t consistently reproduce.
If the bug doesn’t reproduce consistently, there is an entire blog post to write on this matter. This is the time blind bug fixes and guesswork do have their place. Mostly we’ll have to play detective and add more logs to uncover more evidence until the mystery unravels.
2. Find the minimal flow causing the problem to manifest
Once reproduced, before placing breakpoints or looking at logs, it’s best to investigate the faulty scenario a bit more. The goal is to minimize the scenario until you get to the smallest flow where the problem occurs.
For example, the initial scenario might be: The user adds 3 items to the shopping cart, and goes to view that cart. The user removes all 3 items and clicks “Refresh”. An error screen is shown.
On further examination, you might find out that clicking “Refresh” whenever the cart is empty causes the problem. Or, alternatively, removing items and then clicking refresh is the scenario that’s causing the problem. These hints are crucial information that can make the difference between a 2-hour fix and a full day spent banging your head against the wall. Also, with the scenario smaller, debugging takes less time.
3. Debug like a Rock Star
Debugging is a big part of bug solving, and an important skill to master as developers. Most of us spend more time debugging than writing code.
There are many debugging techniques, tools and strategies to know about. The best way to learn debugging techniques is probably pair programming. Doing a debugging session with an experienced developer can teach you a lot. There are also a lot of good blog posts on the subject, like my own 7 Debugging Techniques you should know in C# .NET.
A good first step would be to learn all about your debugging environment, whether it’s Visual Studio or Chrome Dev tools, etc. Know your debugger.
Realize that sometimes, the best debugger in the world is your own brain. With experience in programming and the specific code base I’m working on, I found out the most efficient debugging is another type of debugging: Mental debugging. This involves staring at the code for awhile, debugging the scenario by running the code in my head.
4. Remove code until the bug disappears
Sometimes, you can minimize the problem to a piece of code, but for some reason it’s impossible to debug it and find the specific problem. For example, there’s an exception thrown when loading a UI page.
A good strategy can be to remove parts of code until the problem disappears. In case of UI, you can remove UI elements until the bug stops occurring. Eventually, you will find the problematic element: Simple divide & conquer.
5. Profile to understand what happened under the hood
Sometimes, we have to work on new, unfamiliar projects. Suppose you have to fix a bug in such a new project, where you can reproduce the bug, but have no idea what happened in the code. How do you even know where to place a breakpoint and start debugging?
There are a lot of ways to go about it. You can try to find UI event handlers, guess class names according to functionality or ask someone for help. There is another alternative: Use a performance profiler to “record” and “analyze” the problematic scenario.
Profilers like dotTrace can record a “snapshot” and show all the functions called during that snapshot. You can get a good picture on what happened in code, and where you can start placing breakpoints.
6. When all else fails, go back to last functional version
A special type of bugs are “Regression” bugs. This means functionality that worked in a previous version, but doesn’t work now. These bugs can be quite difficult to solve, since they are often a side effect of a new unrelated functionality recently developed.
As a last resort (since this is very time consuming), you can use your source control to go back in time and do a binary search through your commits until you find the last functional version. Then, move back and forth to find the problematic commit (check-in) that caused problem. Git even has built-in functionality for this called Bisect.
7. Source Control can be a bug detective’s best friend
The source control is a great tool to find bugs. Especially useful for “Regression” bugs. If you know which files in code are related to the bug, go over their history or use Blame (Annotate) to find changes. It’s likely someone recently changed the code, causing the regression bug. Now it’s just a matter of going over the suspect changeset, figuring out the issue.
If the bug is a recent regression, going over the history of the entire repository is also useful.
8. Use Unit Tests to never let it happen again
Once the bug is fixed, make write a test covering it, so it won’t happen again. It can be amazing how many times the same bug is found over and over again in different release versions, or even in the same version. You can be sure that once a bug is found, QA will make sure to write a test case covering that scenario.
9. Make use of Logs, Dumps and Event Viewer
Logs are a great tool, learning to read them and placing new important logs is priceless. For problems in production, logs might be the only thing we have. Same for bugs that don’t consistently reproduce.
When working on a bug, the QA should attach a log file. There is important information to look for in that file. If the bug is a crash, the log will provide the latest log before the crash, or the fatal exception that caused the crash. If you log your exceptions (and you should), looking at the latest logged exception might provide a hint on the bug’s cause.
For bugs that involve the application crashing, learn to analyze Dumps and work with the Event Viewer (on Windows). Both can provide the stack trace from the crash, exception details and even the exact point. For .NET, read about Post-mortem debugging.
10. Master your Tools
Each domain has it’s own set of tools you need to know to become a true debugging expert. For example, for desktop .NET development, you’ll probably want to know:
- A performance profiler like dotTrace
- A memory profiler like redgate ANTS
- OzCode for debugging
- A decompiler like ILSpy
- Snoop for WPF and HawkEye for WinForms
There are countless tools. Some don’t have much value and some are priceless. Knowing your tools can have a huge impact on isolating the problem and finding the bug quickly.
It can be difficult to discover the most useful tools in your domain. If you have a strong team, you can get the best information from your team members. If not, tons of information is available in blog posts, podcasts, Twitter and all other sources of information.
When working for a long time on the same project, you’ll find yourself developing a sort of intuition with bugs. Reading the description and some time staring at the code will replace hours of zombie debugging and mindlessly stepping through code.
Until you reach this level of mastery, there will be a lot of frustration, blaming the compiler itself and maybe cursing the QA department.
When stuck on an “impossible” bug, try to take it easy. You’ll see that all bugs are eventually solved (Well, 99.9% of them). Take a break or work on something else. After a good night’s sleep, we often take a different approach and find the solution quickly. Besides, there’s always some super smart guy or gal around, that’s been working here forever and knows the code by heart. They can always help out.
Have a great bug solving day!
Want to become an expert problem fixer? Check out a chapter from my book Practical Debugging for .NET Developers