Developers commonly face these 10 JavaScript issues

Editor’s note: Our editorial team updated this article on January 18, 2023. We’ve incorporated new sources and ensured it meets our current editorial standards.

JavaScript is essential for practically all modern web applications. Consequently, identifying and resolving JavaScript errors is a primary concern for web developers.

The use of robust JavaScript libraries and frameworks for building single page applications (SPAs), graphics, animations, and server-side platforms is well-established. Given its widespread adoption in web development, mastering JavaScript is crucial.

Initially, JavaScript may appear deceptively simple. Experienced software developers can easily integrate basic JavaScript functionality into web pages. However, the language is far more nuanced, powerful, and intricate than it initially seems. In reality, many of JavaScript’s intricacies can result in various common issues that hinder its functionality – we will delve into 10 of these issues here. Recognizing and sidestepping these problems is vital on your path to becoming a proficient JavaScript developer.

JavaScript Issue No. 1: Incorrect References to this

There’s widespread confusion among JavaScript developers concerning JavaScript’s this keyword.

As JavaScript coding techniques and design patterns have grown more sophisticated, there’s been a surge in self-referencing scopes within callbacks and closures. This trend often leads to “this confusion,” causing JavaScript issues.

Let’s illustrate with a code snippet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const Game = function() {
    this.clearLocalStorage = function() {
        console.log("Clearing local storage...");
    };
    this.clearBoard = function() {
        console.log("Clearing board...");
    };
};

Game.prototype.restart = function () {
    this.clearLocalStorage();
    this.timer = setTimeout(function() {
        this.clearBoard();    // What is "this"?
    }, 0);
};

const myGame = new Game();
myGame.restart();

Running this code produces the following error:

1
Uncaught TypeError: this.clearBoard is not a function

The culprit? Context. When invoking setTimeout(), you’re essentially calling window.setTimeout(). As a result, the anonymous function passed to setTimeout() is defined within the window object’s context, which lacks a clearBoard() method.

A traditional solution, compatible with older browsers, involves storing the reference to this in a variable. This variable can then be inherited by the closure:

1
2
3
4
5
6
7
Game.prototype.restart = function () {
    this.clearLocalStorage();
    const self = this;   // Save reference to 'this', while it’s still this!
    this.timer = setTimeout(function(){
        self.clearBoard();    // Oh, OK, I do know who 'self' is!
    }, 0);
};

Alternatively, in modern browsers, you can use the bind() method to provide the correct reference:

1
2
3
4
5
6
7
8
Game.prototype.restart = function () {
    this.clearLocalStorage();
    this.timer = setTimeout(this.reset.bind(this), 0);  // Bind to 'this'
};

Game.prototype.reset = function(){
    this.clearBoard();    // OK, back in the context of the right 'this'!
};

JavaScript Issue No. 2: Assuming Block-level Scope

As highlighted in our JavaScript Hiring Guide, a frequent point of confusion among JavaScript developers (and consequently, a source of bugs) is the assumption that JavaScript creates a new scope for each code block. While this holds true in many programming languages, it’s not the case in JavaScript.

Take this code, for instance:

1
2
3
4
for (var i = 0; i < 10; i++) {
    /* ... */
}
console.log(i);  // What will this output?

If you anticipate the console.log() call to either output undefined or trigger an error, you’d be mistaken. Surprisingly, it outputs 10. Why?

Unlike most languages where the scope of the variable i would be limited to the for block, this isn’t the case in JavaScript. Here, the variable i persists even after the for loop ends, retaining its last value (a behavior referred to as variable hoisting).

JavaScript does offer support for block-level scopes through the let keyword](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let). The let keyword enjoys extensive support across browsers and server-side JavaScript environments like Node.js. If this is new information, it’s worthwhile to brush up on [scopes, prototypes, and related concepts.

JavaScript Issue No. 3: Memory Leak Generation

Memory leaks are practically unavoidable in JavaScript unless you code with the intent of preventing them. They can occur in numerous ways, so we’ll focus on two prevalent scenarios.

Memory Leak Example 1: Lingering References to Obsolete Objects

Note: This example pertains to older JavaScript engines. Modern engines have garbage collectors (GCs) capable of handling this scenario.

Consider the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var theThing = null;
var replaceThing = function () {
  var priorThing = theThing;  // Hold on to the prior thing
  var unused = function () {
    // 'unused' is the only place where 'priorThing' is referenced,
    // but 'unused' never gets invoked
    if (priorThing) {
      console.log("hi");
    }
  };
  theThing = {
    longStr: new Array(1000000).join('*'),  // Create a 1MB object
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);    // Invoke 'replaceThing' once every second

Running this code and observing memory usage will reveal a significant leak – a full megabyte per second! Even manual garbage collection proves futile. It appears we’re leaking longStr with every replaceThing call. But why?

Without careful coding practices, memory leaks are nearly impossible to avoid in JavaScript.

Let’s delve deeper:

Each theThing object contains its own 1MB longStr object. When replaceThing is called every second, it retains a reference to the previous theThing object in priorThing. One might assume this wouldn’t pose an issue since the prior priorThing should be dereferenced with each iteration (when priorThing is reset via priorThing = theThing;). Furthermore, it’s referenced only within the main body of replaceThing and the unused function unused.

So, why the memory leak?

Understanding this requires a closer look at JavaScript’s inner workings. Typically, each function object maintains a link to a dictionary-like object representing its lexical scope, effectively implementing closures. If both functions defined inside replaceThing utilized priorThing, it would be crucial for them to access the same object, even if priorThing is repeatedly reassigned. This ensures both functions share the same lexical environment. However, the moment any closure uses a variable, it becomes part of the shared lexical environment for all closures within that scope. And this subtle nuance is what results in this troublesome memory leak.

Memory Leak Example 2: Circular References

Consider this code:

1
2
3
4
5
function addClickHandler(element) {
    element.click = function onClick(e) {
        alert("Clicked the " + element.nodeName)
    }
}

Here, onClick has a closure that holds a reference to element (through element.nodeName). By also assigning onClick to element.click, we create a circular reference: elementonClickelementonClickelement

Interestingly, even if element is removed from the DOM, this circular self-reference prevents both element and onClick from being garbage collected, leading to a memory leak.

Preventing Memory Leaks: The Fundamentals

JavaScript’s memory management (especially its garbage collection) relies heavily on the concept of object reachability.

The following are considered reachable objects, known as “roots”:

  • Objects referenced from the current call stack (local variables, parameters of currently invoked functions, and variables in the closure scope)
  • All global variables

Objects remain in memory at least as long as they are reachable from any root through a reference or a chain of references.

The browser’s garbage collector reclaims memory occupied by unreachable objects. In essence, objects are removed from memory if and only if the GC determines them to be unreachable. Unfortunately, it’s easy to end up with defunct “zombie” objects that are no longer needed but are still considered reachable by the GC.

JavaScript Issue No. 4: Equality Confusion

JavaScript offers the convenience of automatic type coercion, converting any value in a boolean context to a boolean value. However, this can sometimes cause more confusion than convenience. For instance, the following expressions often trip up JavaScript developers:

1
2
3
4
5
6
7
8
9
// All of these evaluate to 'true'!
console.log(false == '0');
console.log(null == undefined);
console.log(" \t\r\n" == 0);
console.log('' == 0);

// And these do too!
if ({}) // ...
if ([]) // ...

Regarding the last two expressions, while they are empty (which might suggest they would evaluate to false), both {} and [] are in fact objects, and any object will be coerced to a boolean value of true in JavaScript, consistent with the ECMA-262 specification.

These examples highlight that type coercion rules can be unclear. Therefore, unless type coercion is explicitly desired, it’s best to use === and !== (instead of == and !=) to avoid unintended side effects. (== and != perform automatic type conversion during comparison, while === and !== compare without type conversion.)

While on the topic of type coercion and comparisons, it’s crucial to remember that comparing NaN with anything (including NaN itself) always returns false. Therefore, you cannot rely on equality operators (==, ===, !=, !==) to determine if a value is NaN. Instead, utilize the built-in global function isNaN():

1
2
3
console.log(NaN == NaN);    // False
console.log(NaN === NaN);   // False
console.log(isNaN(NaN));    // True

JavaScript Issue No. 5: Inefficient DOM Manipulation

JavaScript simplifies DOM manipulation (adding, modifying, and removing elements), but it doesn’t inherently promote doing so efficiently.

One common example is code that sequentially adds a series of DOM elements. Since adding a DOM element is resource-intensive, code that does this repeatedly is inefficient and likely to perform poorly.

An efficient alternative for adding multiple DOM elements is to use document fragments](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment), which enhances [performance and efficiency.

For example:

1
2
3
4
5
6
7
8
const div = document.getElementById("my_div");
const fragment = document.createDocumentFragment();
const elems = document.querySelectorAll('a');

for (let e = 0; e < elems.length; e++) {
    fragment.appendChild(elems[e]);
}
div.appendChild(fragment.cloneNode(true));

Apart from inherent efficiency gains, creating attached DOM elements is costly. Conversely, creating and modifying detached elements and then attaching them offers significantly better performance.

JavaScript Issue No. 6: Misusing Function Definitions Inside for Loops

Consider this code:

1
2
3
4
5
6
7
var elements = document.getElementsByTagName('input');
var n = elements.length;    // Assume we have 10 elements for this example
for (var i = 0; i < n; i++) {
    elements[i].onclick = function() {
        console.log("This is element #" + i);
    };
}

In this scenario, if there were 10 input elements, clicking any of them would display “This is element #10”! Why? Because by the time any element’s onclick is triggered, the for loop will have completed, and the value of i will be 10 for all elements.

Here’s how to rectify this JavaScript issue and achieve the desired outcome:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var elements = document.getElementsByTagName('input');
var n = elements.length;    // Assume we have 10 elements for this example
var makeHandler = function(num) {  // Outer function
     return function() {   // Inner function
         console.log("This is element #" + num);
     };
};
for (var i = 0; i < n; i++) {
    elements[i].onclick = makeHandler(i+1);
}

In this revised code, makeHandler executes immediately with each loop iteration, receiving the current value of i+1 and binding it to a scoped num variable. The outer function returns the inner function (which also uses the scoped num variable), and the element’s onclick is set to this inner function. This ensures that each onclick receives and utilizes the correct i value (via the scoped num variable).

JavaScript Issue No. 7: Underutilizing Prototypal Inheritance

A surprisingly large number of JavaScript developers don’t fully grasp and, therefore, underutilize the capabilities of prototypal inheritance.

Here’s a basic example:

1
2
3
4
5
6
7
BaseObject = function(name) {
    if (typeof name !== "undefined") {
        this.name = name;
    } else {
        this.name = 'default'
    }
};

This appears straightforward: if a name is provided, use it; otherwise, default to ‘default’. For instance:

1
2
3
4
5
var firstObj = new BaseObject();
var secondObj = new BaseObject('unique');

console.log(firstObj.name);  // -> Results in 'default'
console.log(secondObj.name); // -> Results in 'unique'

But consider this scenario:

1
delete secondObj.name;

This would result in:

1
console.log(secondObj.name); // -> Results in 'undefined'

Wouldn’t it be more elegant for this to revert to ‘default’? We can easily achieve this by leveraging prototypal inheritance:

1
2
3
4
5
6
7
BaseObject = function (name) {
    if(typeof name !== "undefined") {
        this.name = name;
    }
};

BaseObject.prototype.name = 'default';

In this version, BaseObject inherits the name property from its prototype object, where it’s initially set to 'default'. Consequently, if the constructor is called without a name, it defaults to default. Similarly, removing the name property from a BaseObject instance causes the prototype chain to be searched, retrieving the name property with the value 'default' from the prototype object. Now, we get:

1
2
3
4
5
var thirdObj = new BaseObject('unique');
console.log(thirdObj.name);  // -> Results in 'unique'

delete thirdObj.name;
console.log(thirdObj.name);  // -> Results in 'default'

JavaScript Issue No. 8: Creating Faulty References to Instance Methods

Let’s define a simple object and create an instance:

1
2
3
4
5
6
7
var MyObjectFactory = function() {}
	
MyObjectFactory.prototype.whoAmI = function() {
    console.log(this);
};

var obj = new MyObjectFactory();

Now, for convenience, we’ll create a reference to the whoAmI method, intending to access it simply as whoAmI() instead of obj.whoAmI():

1
var whoAmI = obj.whoAmI;

To confirm that we’ve indeed stored a function reference, let’s print the value of our new whoAmI variable:

1
console.log(whoAmI);

Output:

1
2
3
function () {
    console.log(this);
}

So far, so good.

But observe the difference when invoking obj.whoAmI() versus our convenient reference whoAmI():

1
2
obj.whoAmI();  // Outputs "MyObjectFactory {...}" (as expected)
whoAmI();      // Outputs "window" (uh-oh!)

What went wrong? Our whoAmI() call resides in the global namespace, so this is set to window (or undefined in strict mode), not the obj instance of MyObjectFactory! In essence, the value of this depends on the calling context.

Arrow functions ((params) => {} instead of function(params) {}) provide a static this that’s not determined by the calling context like regular functions. This offers a workaround:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var MyFactoryWithStaticThis = function() {
    this.whoAmI = () => { // Note the arrow notation here
        console.log(this);
    };
}

var objWithStaticThis = new MyFactoryWithStaticThis();
var whoAmIWithStaticThis = objWithStaticThis.whoAmI;

objWithStaticThis.whoAmI();  // Outputs "MyFactoryWithStaticThis" (as usual)
whoAmIWithStaticThis();      // Outputs "MyFactoryWithStaticThis" (arrow notation benefit)

You might notice that although we achieved the desired output, this references the factory, not the instance. Instead of further attempts to fix this, it’s worth exploring JavaScript approaches that don’t rely on this (or even new), as discussed in As a JS Developer, This Is What Keeps Me Up at Night.

JavaScript Issue No. 9: Passing a String as the First Argument to setTimeout or setInterval

Firstly, let’s clarify: Providing a string as the first argument to setTimeout or setInterval is not inherently wrong. It’s perfectly valid JavaScript code. The concern here is more about performance and efficiency. What’s often overlooked is that passing a string as the first argument to these methods leads to the function constructor being used to convert it into a new function. This process can be slow, inefficient, and usually unnecessary.

The alternative is to pass a function directly. Let’s illustrate with an example.

A typical (but less efficient) use of setInterval and setTimeout involves passing a string as the first parameter:

1
2
setInterval("logTime()", 1000);
setTimeout("logMessage('" + msgValue + "')", 1000);

The preferred approach is to pass a function as the initial argument:

1
2
3
4
5
setInterval(logTime, 1000);   // Passing the logTime function to setInterval
	
setTimeout(function() {       // Passing an anonymous function to setTimeout
    logMessage(msgValue);     // (msgValue is still accessible in this scope)
}, 1000);

JavaScript Issue No. 10: Neglecting “Strict Mode”

As explained in our JavaScript Hiring Guide, “strict mode” (enabled by including 'use strict'; at the beginning of your JavaScript files) enforces stricter parsing and error handling during runtime, enhancing code security.

While not using strict mode isn’t a “mistake,” its adoption is increasingly being encouraged, and its omission is often considered bad practice.

Key benefits of strict mode include:

  • Simplified debugging. Code errors that would have been silently ignored now generate errors or throw exceptions, providing early problem detection and faster debugging.
  • Prevention of accidental globals. Without strict mode, assigning a value to an undeclared variable implicitly creates a global variable. This is a common JavaScript error. Strict mode throws an error in such cases.
  • Elimination of this coercion. In non-strict mode, a this value of null or undefined is automatically coerced to the globalThis variable, potentially causing frustrating bugs. Strict mode throws an error instead.
  • Prohibition of duplicate property names and parameter values. Strict mode flags duplicate named properties in objects and duplicate named function arguments, catching potential bugs early on.
  • Enhanced eval() security. Strict mode changes how eval() behaves, most notably preventing variables and functions declared inside an eval() statement from being created in the containing scope (which can cause issues in non-strict mode).
  • Error on invalid delete usage. The delete operator (for removing object properties) cannot be used on non-configurable properties. Non-strict code fails silently in such cases, while strict mode throws an error.

Reducing JavaScript Issues With a Smarter Approach

As with any technology, a deeper understanding of JavaScript’s inner workings leads to more robust code and better utilization of the language’s capabilities. Conversely, a lack of proper understanding is a breeding ground for JavaScript problems. Familiarizing yourself with the language’s nuances and subtleties is the most effective way to enhance your proficiency and productivity.

Licensed under CC BY-NC-SA 4.0