Tips on debugging client-side JavaScript

| 7 min. (1359 words)

Debugging client-side JavaScript can be a frustrating experience. The environment in which your code is running means a large number of things could go wrong. Race conditions, browser feature compatibility, dependency issues and resources failing to download – can seem outside of your control.

These issues can be handled by reducing the number of points of failure and simplifying how assets are loaded. Another main source of frustration can come from the language of JavaScript itself. Relying on the language to enforce good practices or even consistent syntax (like many other modern languages now do) simply doesn’t work.

In this article, I’ve outlined a few tips on debugging client-side Javascript, and also how to avoid bugs as you develop.

New-lines and semicolons

In JavaScript new-lines and semicolons have unexpected behavior in some cases. A language feature called ASI (Automatic Semicolon Insertion) causes statements which aren’t terminated by semicolons to be implicitly appended with one in cases such as the code being syntactically invalid.

For example:

while (a < b) {
 ...
 a++
}

Would effectively be the same as:

while (a < b) {
 ...
 a++;
}

This may seem like a nice language feature which enables you to reduce the amount of typing and syntactic clutter. However, it can lead to ambiguous and sometimes incorrect code. 

For example:

function calculate(a, b) {
 var c =
        a + b
 return
        c
}

The above function looks like it should calculate a value, place it in a local variable then return that variable. In actuality it calculates the variable and assigns it to variable c, then returns undefined .

While this may not be the most normal way to format your code, in some cases it may be neater, especially if you are working with long expressions. It’s good to keep in mind that in cases like these the newline character after the return statement causes a semicolon to be automatically inserted.

Always explicitly end statements with semicolons and ensure return statements are always followed by the value to be returned on the same line.

Deserialization and ‘eval’

Serializing and deserializing JavaScript objects is a common operation, especially when communicating with various web APIs and storage mechanisms. It’s important to be careful when deserializing as you can easily execute foreign code if you’re not careful.

One way is the eval function. This evaluates any string as if it were JavaScript code. As running ‘eval’ on a serialized object would return an object literal, this may seem like a good way to deserialize data like foreign API responses, serialized objects in cookies or local storage. However, if somehow the serialized payload was modified by an attacker he could easily inject statements which steal user credentials or perform harmful actions.

One way is the eval function. This evaluates any string as if it were JavaScript code. As running ‘eval’ on a serialized object would return an object literal, this may seem like a good way to deserialize data like foreign API responses, serialized objects in cookies or local storage. However, if somehow the serialized payload was modified by an attacker he could easily inject statements which steal user credentials or perform harmful actions.

The safe way to deserialize an object is to use parse  method on the JSON  object.

Don’t do:

var myObject = eval(mySerializedObject);

Do:

var myObject = JSON.parse(mySerializedObject);

Dynamic types

JavaScript is a dynamically typed language. This means that variables can store values of any type and this could change at any time. However, it’s a good step to remember when you are debugging client-side JavaScript.

Eg:

var a = 'apple';                       //a is a string
a = 5;                                 //a is a number
a = ['apple', 'banana', 'carrot'];     //a is an array of strings
a = function(a, b) { return a * b; };  //a is a function which expects two numbers as arguments

Because of this capability, many issues may arise when a variable is holding a value type you didn’t expect. Therefore, how you name and utilize variables is very important. Generally it’s not a good idea to reuse the same variable for different value types over and over unless under very specific controls. This is especially important in code with lots of branches. If the same value is being modified again and again it may be hard to reason about what the variable contains at any one point in the code, which is likely to lead to bugs.

Some good practices include only assigning a variable once. This is common in functional programming. If you enforce this yourself, you shouldn’t run into issues where you don’t know what something is because it will always be the same as what it was initially assigned (assuming it hasn’t mutated itself).

Another consideration related to dynamic typing is that the type of a variable may automatically be cast to another type in some cases. For instance if we pass the string ‘5’   into a function function(input) { return input * 10; }  we may expect an error as a string cannot intuitively be multiplied, however the function will actually return the value 50  . This is because the value ‘5’   has been implicitly converted to a number before being multiplied.

In this example it may be relatively easy to understand why and what happened but in larger applications where objects with many fields may be being operated on an implicit type conversion such as above may result in situations which you wouldn’t expect.

For this reason it’s important to (in cases of ambiguous types) check the type of the variables acted upon and handle accordingly.

For example the ‘example’ function could handle this with an exception – this would be helpful mainly for debugging and determining where and why this value is being passed.

function(input) {
 if (typeof(input) !== 'number') {
   throw new Error('Invalid argument type');
 }
 return input * 10;
}

Another consideration is related to equality checking. Modern JavaScript provides two means for checking the equality of values: loose equality is checked using the == operator, values may be cast to other types automatically upon comparison using this operator, this means values such as ‘5’ and 5 will reported as equal.

The strict equality operator is ===  . The type of the value must be equal for this to evaluate to true, ‘5’ === 5  would evaluate to false. 5 === 5  or ‘5’ === ‘5’  would evaluate to true .To avoid issues related to equality casting it’s recommended to always use triple equals for equality comparison.

Browser support and feature detection

A large number of issues can come about when users open your website in an old browser or simply one you haven’t tested with. These issues could be in the form of rendering abnormalities (or worse, malfunctioning code) which causes your webpage to not work at all.

One way to avoid issues such as these is to make less assumptions about the capabilities of the browsers clients may use. This can be done by feature checking. A library such as Modernizr can help you determine whether or not a browser has the features you wish to utilize. You can then write fallback methods for the cases where features are not supported so users get at best a reimplementation with other technology or in the worst case a tidy explanation about why something isn’t supported.

Another way to avoid issues pertaining to lacking features is to provide a backup implementation of a browser feature yourself. This is called a polyfill.

A very basic polyfill might look something like this:

Math.pow = Math.pow || function(a, b) {
 return a ** b;
};

This simply ensures the function Math.pow exists even if the browser doesn’t natively fulfill the functionality.

In real world cases the polyfill for a function may well be very complicated and it may be best to find a 3rd party library which provides the polyfill necessary.

Conclusion

Although JavaScript has a reputation for being a little unstructured, the above considerations will help alleviate any time spent debugging client-side JavaScript later.

Find out more about JavaScript error handling best practices in our Ultimate Guide To JavaScript Error Monitoring.