Principles of Debugging Web Applications


Regardless of our coding skills we all make mistakes. Bugs do not only appear in our own code. Most web-applications are created by teams, and your teammates have the same right to make mistakes as you do. In addition, inheriting legacy code also means inheriting bugs from another team. In these situations, debugging skills come very handy.

It is worth investing some time and effort in fine tuning our debugging skills. I will show you a couple of tips to widen your debugging arsenal. I will also give you ideas that let you locate bugs with less work.

This article will mainly be about principles of debugging. We won’t go into technological details yet and we won’t explore tips and tricks related to browser developer tools. The next article will be more technical, detailing debugging tools applied in practice.

Know your outcome

Your most important debugging tool is your mind. I can show you hundreds of debugging tips and tricks, but they only help you if you have enough domain-specific knowledge to use them in context. Do not even try using these techniques before you have a good idea on what the problem is.

Debugging tools are for the purpose of verifying your assumptions. Prior understanding of what the code should do is essential.

If you realize that you have inserted ten breakpoints in ten different areas of your code, you are doing something wrong. If you know where the problem is, you should also know where to insert your breakpoints. The same holds for logging messages. If you have ten different messages, you don’t know what you are looking for. Do your homework and try to understand the code without executing it.

A playground helps a lot

Always write code in a testable manner. Assuming that you regularly unit test your code, you should be able to create your business objects and invoke their methods in a playground. The easiest playground is the developer tools of your choice. You can use the console to create objects and invoke their methods. It also helps if you can access your application in debug mode, where Mocha, Chai and SinonJs are accessible.

The basic training of a developer team should include practice on how to play with the objects of your application.

A playground can be used for multiple purposes:
– verify your mental model of the code by observing outputs belonging to the inputs of your choice
– black box testing of some methods to verify your assumption about the fault
– reproducing the failure

Fix the cause, not the symptom

I used the words fault and failure on purpose in the previous section. If you don’t yet know the difference between fault, error and failure, read the section “Fault, error, failure” of this post.

Human health is quite analogous to bug fixing. Suppose that patient John suffers from a vitamin-shortage. As a result, he catches cold. The doctor tells him to take paracetamol to ease the symptoms. Paracetamol hides most of the symptoms until John gets better. After the disease is gone, John does not even think about changing his diet.

When it comes to code, the condition is badly engineered spaghetti-code. The disease is a fault written by a developer who failed to fully understand the code. Given that this bug caused a loss of 10000€, the company puts the developer team under pressure to immediately fix the bug. The developers quickly learn what the failure looks like and treat it with some paracetamol:

Similarly to very high fever, in case of emergency, the symptoms have to be treated immediately. The fix appears to work, the developers deploy the fix to production. Often times, the cause is forgotten. What went wrong?

In the long run, paracetamol destroys your liver cells. Code for symptom treatments destroys the maintainability of the software. The quick fix should always be discarded and the real cause should be fixed properly.

If the bug fix is not an emergency, always look for the cause. Make sure you fully understand the failure, the propagation of errors and the actual fault. Also make sure that you fix the fault respecting the roles and responsibilities of your business objects. For instance, in code written according to the Model-View-Presenter paradigm, never fix the errors of the model in the presenter.

In case the code is hard to debug or change, consider refactoring it. This leads to the next section.

Hard to debug code needs refactoring work

Whenever developers have a hard time testing, debugging or changing code, think about refactoring. Refactoring comes with the benefit of increasing the maintainability of the code while keeping behavior intact.

Some signs of badly engineered code are:

  • WET code (We Enjoy Typing)
  • Unclear roles and responsibilities
  • Very long methods, high cyclomatic complexity
  • Bad/unclear names
  • Repeated switch statements in multiple methods of the same class referring to different subtypes of the class

If code is hard to understand, bugs will eventually appear. Even stakeholders should be aware of the need for a healthy ratio between new features and maintenance. Fixing a fault you are aware of is always cheaper than debugging failures caused by it.

Test one assumption at a time

Recall that you have to make an assumption on what went wrong in the code. This assumption can be tested with just a few console logs and breakpoints. Alternatively, you can insert one breakpoint in the faulty context and create a playground using the dev tools console.

Whenever your assumption is proven wrong, discard your breakpoints and console logs before verifying your next assumptions. If you want to return to these logs and breakpoints later, stash it using Git.

Commenting out logs and breakpoints does not make sense. You lose track of which debugging statement belongs to which of your assumptions.

Once debugger statements and logs stack up in your code, they generate noise. This noise distracts you from locating the bug using a clear thought process.

Test Driven Bugfixing

One way to reproduce a bug is to write a failing test based on the bug description. Even if the test passes, your work is not for nothing: you have one more test guarding the application from bugs. Eventually you will catch the failure with a failing test.

Trace back the error propagation and locate the fault. As soon as you understand the fault, guard it with failing tests. These tests will guide you in fixing the fault. The main benefit of these tests are that the same bug will never appear on production again as long as you keep executing your unit tests before each and every deployment.

Do not debug on production

I have heard of horror stories when debugging code was deployed to production just because some special data was only found in the live database. This is amateur work.

Let me give you some tips on how to prepare for situations like this.

If developers have access to the live database, cloning the database is one option. In some cases, it may take very long. The solution is a specialized dumping script that copies relevant data to the developer database.

It can happen that developers are not allowed to access some sensitive data. The usual bad practice in this case is that developers receive a temporary account whenever the bug hurts the company badly enough. Any solution is better than that. Let it be a dumping script that creates anonymous data of the same sort, or a refined bug report process that gives developers enough data and enough chance for additional questions, your team should always have a plan for handling emergencies.

Regardless the processes and access rights, make sure that the databases of the development environment are updated regularly with fresh data of the same magnitude as on production. It helps not only in reproducing nasty bugs, but also in locating performance bottlenecks.

As a front end developer, you may encounter bugs that only appear using special devices or special browsers. Alternatively, an ad blocker software may interfere with your AJAX requests. Try to gather as much information from the environment as possible, and make sure there is a way to communicate with the user who encountered the failure.

A more proactive approach to catch bugs that only appear under special circumstances is to use an error tracker application such as SherlogJs. Errors will appear on your dashboard before the user considers sending you a bug report. Since many bugs are never reported, this is the only way to catch these failures.

There is another reason why bugs cannot be reproduced in a development environment. External libraries and additional services may force you to gather information on production. Fixing such bugs requires dedicated support and smooth cooperation with other teams. It is always important to establish services that are testable without depending on the production environment.

Summary

After reproducing a bug based on a bug description, start thinking about what went wrong. Formulate assumptions and study the code. You should have a good idea of which class and which methods may be affected. Play with the relevant business objects to verify your assumptions on the code and the bug. While playing with the objects or debugging, always check one assumption at a time to eliminate distractions. Always fix the fault, not the failure. If the code is hard to debug or fix, consider refactoring it. Make sure your fix is fully covered with tests. In case the failure is hard to reproduce, think about improving your infrastructure to support local debugging.

Hopefully you are not disappointed to conclude that this article is about to end and I did not address how the debugger statement works or how you should use the developer tools of your browser. Debugging is more than just tools. Hopefully you will find some of these ideas useful and become better in catching bugs. If you are interested in debugging tools, stay tuned for the next article.