Software development would be quick and simple if only everything just worked — from network to services to libraries to our own code. Test environment is always up and fast. Data response follows the contract exactly. Libraries integrated without a hitch. All browsers follow standards perfectly and behave the same. The list goes on. Unfortunately, this perfect developer world does not exist. There are bugs, quirks, and problems. On any given day, few things work as expected right away.
A good part of our development time is spent debugging. It can take hours or days, and sometimes, even weeks. But it doesn't have to be that way. Just like any skill, debugging is an art with its own techniques. I will present some that have helped me and others shorten debugging time and make the process less frustrating.
(Note: The following techniques are tool-agnostic. They help whether you use Chrome DevTools, Safari Web Inspector, Firebug, command-line node inspector, Visual Studio Code built-in debugger, etc.)
First, before you dive into a debug session, keep three principles in mind.
#1: The computer ALWAYS does exactly what you tell it to. If it doesn't do what you expect, you didn't tell it correctly.
I am paraphrasing here, but that's what my CS101 professor once said to the class. It's one of the few things I still remember from college. Whenever I am stuck, these words pop up in my mind. It reminds me that debugging is a solvable problem - there is hope and you will find a solution. (In a real development setting, your code may not be the issue, it may be an issue in the library or service dependency.)
#2: Very, very, very few behaviors are intermittent or random.
"Intermittent" issues can almost always be reproduced consistently using clearly written steps that are carried out precisely. They look intermittent only because the steps are not clear or complete enough, or the steps are not being followed precisely each time the test is run.
#3: Don't start fixing until you have found the root cause.
We often start "fixing" an issue before we have even identified a cause, let alone the root cause. Without knowing the cause, a "fix" is not a fix. It's a patch. And it will likely cause more issues than it fixes.
Debugging may take a while. It may involve dozens of rebuilds, file saves, server restarts, browser refreshes, etc. It's tedious and soon becomes grunt work. So, every second or minute shaved from these actions will shorten iteration time and make it just a little more bearable. Some common time-savers that many overlook:
- Are you using too much mouse and too few keyboard short-cuts?
- Are you using node-watch?
- Are you using ESLint to catch syntax and other fatal but inconspicuous errors in real-time?
- Has the code been refactored to a good state for easier debugging? E.g., cache variables, little or no duplicate code.
- Do you have a step-by-step plan, as detailed as necessary, to test your code?
A detailed test plan is key to effective debugging
Creating a detailed test plan may take 30 minutes, but for debugging that can take hours or days, the return on investment is high. It also forces you to think about the problem before jumping in. A few pointers:
- Check whether a bug happens on a common setup
Does it really happen only on the German site using Edge 17 on a specific Android phone set to Italian? Or can you reproduce it on the US site using the latest desktop version of Chrome?
- Don't take the steps documented in a bug ticket at face value. Try to simplify the test flow.
Do you really have to go from Home page to Search page to Item page to Checkout page, then add an address, add a coupon, change the address and then remove the coupon, and then add a credit card to see the issue? Perhaps you can see the issue simply by going from Item page to Checkout page and add a credit card?
- Reduce data input
When you are testing the email field in a form, email@example.com probably works the same as firstname.lastname@example.org
- Minimize typing
Turn on browser auto-fill with pre-populated test data. Or copy and paste from a notepad. Just make sure to follow the test steps exactly.
- Use mock data
Is a live service required to debug? Would mock data work?
- Write unit tests
Write unit test to help debug? Yes. When your code change is related to business logic, it's worth the extra time to write unit tests. Make a change, run the tests. It is ALWAYS faster than manual testing. Besides, unit tests help ensure that your solution does not break existing functionality.
Once your test plan is set, follow each step EXACTLY
A seemingly trivial inconsistency in a step during user interface debugging can make a behavior look intermittent.
- Typing one letter at a time; Control-C; Right-click -> Paste are not the same.
- How quickly did you click on a button after page load?
- Back chevron, browser back button, and keyboard combination all trigger different actions.
- Remember the speed you type, the speed you scroll.
- Does the plan say "click, type, type, click"? Or "type, click, type, click"?
- What command do you use to start the server?
- Do you delete your workspace or browser cache before you run the test plan?
I often sit with other developers to debug together. Most of the time, one of these tips leads to a solution.
Are you debugging the right file? In the right workspace? Correct URL?
Ever spent hours making changes, but the changes don't seem to be reflected? You re-yarned or re-npm — that didn't work. You re-cloned the git repository — that didn't work. You cleared the workspace and browser cache — that didn't work either
If in doubt, check the workspace folder name and the application test URL.
Add an alert. Log something. Does it show up?
Delete the workspace. Does the page still load?
Is there an error? Did you read it?
The error stack in the browser console or terminal may look long, cryptic, and scary. Many of us tend to ignore it until hours of fruitless debugging have passed. Don't do that. Try to read and understand it right away. It usually provides the exact place of the problem, or at least a hint on where to start. It can save you hours of aimless retries.
Are you calling a library method correctly?
Read (or re-read) the doc. Then, read the library code itself.
If you have an idea, stop wondering whether it will work. Try it.
Don't wonder. Don't wait. Just try it. A "No" means you can move to the next idea. The beauty of software (vs. hardware) is that if something breaks, you simply restart. No need to buy that expensive, hard-to-find replacement part.
Is the response or input correct?
Don't assume the service response or the input to a form or a function is correct or what you expect it to be.
Network issues, service failure, or buggy upstream code can contribute to bad responses. A library method or your own code may have modified the input. Put logs at different points on the code path and verify. You can also set breakpoints to verify inputs and outputs, though my experience is that skimming and searching the log is usually faster.
Did you debug that library method?
Most applications will likely use open-sourced modules. In general, these modules, especially the popular ones, are of high quality, but any code can have bugs. If you suspect a library method may be the culprit, check it.
Start removing code — binary elimination
During debugging, nothing drains your enthusiasm more than staring at a large file or a method with dozens of paths. Isolate the problem by eliminating files or code half at a time. Less code, easier to isolate problems.
- Remove half the files/changes, test
- Remove half of the remaining files/changes, test again
- Continue halving and testing as needed
Once the problem is fixed, restore the removed code. Test.
Are you trying the same code change and hoping for a different result?
Don't. It won't. Unless you are not running test steps consistently, which you should.
The issue is caused by unstashed changes
Sometimes, you come across a bug only after days of code changes. Thinking that you introduced it, you start to add and remove code to fix the bug. Stop. First check whether the bug was already there before your changes. Stash the changes to confirm.
Corollary: When your code is in a good state, commit it. Small commits are good as long as each commit contains a coherent set of changes. You can always squash the commits later during code merge.
Why does it work for your teammate but not for you?
Do you have the same setup? Here is a checklist:
- Same environment?
- Same configs?
- Same branch? The right branch?
- Did you pull the latest code?
- Unstashed changes?
- Did you install dependencies recently?
- Did you clear your workspace cache or temp folder?
- Same version of node / nvm?
- Same version of yarn?
- Same code editor? Same version?
- How do you start your server?
- Same browser?
- Same device?
- Same URL?
While developing and debugging, remember this:
- Test frequently after making small changes.
- Keep a record of what you have tried.
- Re-trying the same thing won't give you a different result, if you adhere to Principle #2 .
When you think you have the fix:
Don't check in yet. First, confirm that it is really fixed and not a fluke. Run through your entire test plan multiple times. Do this:
- Remove the fix, confirm it's broken.
- Add the fix back, confirm it's fixed.
- Remove the fix, confirm it's still broken. (Yes, again. You may be surprised.)
- Add the fix back, confirm it's still fixed.
I have been at this as long as I can remember. I am still stuck.
Please stop and take a break. If that doesn't help, call it a day. Go to bed. Don't think about it. You will have a different perspective, more ideas, and perhaps a solution the next day.
Time to get help
Finally, after hours or days of interminable frustration, walk over to a teammate and debug together.
And as magical as it may sound, sometimes, the gremlins residing in your code may just decide to give you a break and everything starts to work suddenly, just for your walking over to someone. It does happen. I have been there.
I hope these tips will help you resolve issues more quickly, so you can move on to better things. Perhaps a critical feature no one has time to pick up. Perhaps an interesting side project. Perhaps help your teammates debug using these techniques. With the time saved, what you do is up to you.