Web Front-end13-minute read

The 10 Most Common JavaScript Issues Developers Face

At first, JavaScript may seem quite simple. Yet the language is significantly more nuanced, powerful, and complex than one would initially be led to believe. Many of JavaScript’s subtleties lead to a number of common problems—10 of which we discuss here—that keep code from behaving as intended. It’s important to be aware of and avoid these pitfalls in one’s quest to become a master JavaScript developer.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

At first, JavaScript may seem quite simple. Yet the language is significantly more nuanced, powerful, and complex than one would initially be led to believe. Many of JavaScript’s subtleties lead to a number of common problems—10 of which we discuss here—that keep code from behaving as intended. It’s important to be aware of and avoid these pitfalls in one’s quest to become a master JavaScript developer.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Ryan J. Peterson
Verified Expert in Engineering

Ryan is an architect, entrepreneur, and developer. He is skilled in building cloud-scalable, extensible software systems.

Previously At

Automation Anywhere
Share

Editor’s note: This article was updated by our editorial team on January 18, 2023. It has been modified to include recent sources and to align with our current editorial standards.

Today, JavaScript is at the core of virtually all modern web applications. That’s why JavaScript issues, and finding the mistakes that cause them, are at the forefront for web developers.

Powerful JavaScript-based libraries and frameworks for single page application (SPA) development, graphics and animation, and server-side JavaScript platforms are nothing new. JavaScript has become ubiquitous in the world of web app development and is therefore an increasingly important skill to master.

At first, JavaScript may seem quite simple. Indeed, building basic JavaScript functionality into a web page is a fairly straightforward task for any experienced software developer, even if they’re new to JavaScript. Yet the language is significantly more nuanced, powerful, and complex than one would initially believe. In fact, many of JavaScript’s subtleties can lead to a number of common problems that keep it from working—10 of which we discuss here. It is important to be aware of and avoid these issues on your journey to become a master JavaScript developer.

JavaScript Issue No. 1: Incorrect References to this

There’s no shortage of confusion among JavaScript developers regarding JavaScript’s this keyword.

As JavaScript coding techniques and design patterns have become increasingly sophisticated over the years, there’s been a corresponding increase in the proliferation of self-referencing scopes within callbacks and closures, which are a fairly common source of “this confusion” causing JavaScript issues.

Consider this example code snippet:

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();

Executing the above code results in the following error:

Uncaught TypeError: this.clearBoard is not a function

Why? It’s all about context. The reason you get that error is because, when you invoke setTimeout(), you are actually invoking window.setTimeout(). As a result, the anonymous function being passed to setTimeout() is being defined in the context of the window object, which has no clearBoard() method.

A traditional, old-browser-compliant solution is to simply save your reference to this in a variable that can then be inherited by the closure, e.g.:

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 newer browsers, you can use the bind() method to pass in the proper reference:

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: Thinking There Is Block-level Scope

As discussed in our JavaScript Hiring Guide, a common source of confusion among JavaScript developers (and therefore a common source of bugs) is assuming that JavaScript creates a new scope for each code block. Although this is true in many other languages, it is not true in JavaScript. Consider, for example, the following code:

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

If you guess that the console.log() call would either output undefined or throw an error, you guessed incorrectly. Believe it or not, it will output 10. Why?

In most other languages, the code above would lead to an error because the “life” (i.e., scope) of the variable i would be restricted to the for block. In JavaScript, though, this is not the case, and the variable i remains in scope even after the for loop has completed, retaining its last value after exiting the loop. (This behavior is known as variable hoisting.)

Support for block-level scopes in JavaScript is available via the let keyword. The let keyword has been widely supported by browsers and back-end JavaScript engines like Node.js for years now.If that’s news to you, it’s worth taking the time to read up on scopes, prototypes, and more.

JavaScript Issue No. 3: Creating Memory Leaks

Memory leaks are almost inevitable issues in JavaScript if you’re not consciously coding to avoid them. There are numerous ways for them to occur, so we’ll just highlight two of their more common occurrences.

Memory Leak Example 1: Dangling References to Defunct Objects

Note: This example applies to legacy JavaScript engines only—modern ones have garbage collectors (GCs) that are smart enough to handle this case.

Consider the following code:

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

If you run the above code and monitor memory usage, you’ll find that you’ve got a significant memory leak—a full megabyte per second! And even a manual garbage collector doesn’t help. So it looks like we are leaking longStr every time replaceThing is called. But why?

Memory leaks are almost inevitable issues in JavaScript if you’re not consciously coding to avoid them.

Let’s examine things in more detail:

Each theThing object contains its own 1MB longStr object. Every second, when we call replaceThing, it holds on to a reference to the prior theThing object in priorThing. But we still wouldn’t think this would be a problem, since, each time through, the previously referenced priorThing would be dereferenced (when priorThing is reset via priorThing = theThing;). Moreover, it is only referenced in the main body of replaceThing and in the function unused, which is, in fact, never used.

So again we’re left wondering why there is a memory leak here.

To understand what’s going on, we need to better understand the inner workings of JavaScript. Closures are typically implemented by every function object linking to a dictionary-style object representing its lexical scope. If both functions defined inside replaceThing actually used priorThing, it would be important that they both get the same object, even if priorThing gets assigned over and over so that both functions share the same lexical environment. But as soon as a variable is used by any closure, it ends up in the lexical environment shared by all closures in that scope. And that little nuance is what leads to this gnarly memory leak.

Memory Leak Example 2: Circular References

Consider this code fragment:

function addClickHandler(element) {
    element.click = function onClick(e) {
        alert("Clicked the " + element.nodeName)
    }
}

Here, onClick has a closure that keeps a reference to element (via element.nodeName). By also assigning onClick to element.click, the circular reference is created, i.e., elementonClickelementonClickelement

Interestingly, even if element is removed from the DOM, the circular self-reference above would prevent element and onClick from being collected and thus would become a memory leak.

Avoiding Memory Leaks: The Essentials

JavaScript’s memory management (and, in particular, its garbage collection) is largely based on the notion of object reachability.

The following objects are assumed to be reachable and are known as “roots”:

  • Objects referenced from anywhere in the current call stack (that is, all local variables and parameters in the functions currently being invoked, and all the variables in the closure scope)
  • All global variables

Objects are kept in memory at least as long as they are accessible from any of the roots through a reference or a chain of references.

There is a garbage collector in the browser that cleans memory occupied by unreachable objects; in other words, objects will be removed from memory if and only if the GC believes that they are unreachable. Unfortunately, it’s fairly easy to end up with defunct “zombie” objects that are no longer in use but that the GC still thinks are reachable.

JavaScript Issue No. 4: Confusion About Equality

One JavaScript convenience is that it will automatically coerce any value being referenced in a boolean context to a boolean value. But there are cases in which this can be as confusing as it is convenient. The following expressions, for example, are known to be troublesome for many a JavaScript developer:

// 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 ([]) // ...

With regard to the last two, despite being empty (which might lead you to believe that 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.

As these examples demonstrate, the rules of type coercion can sometimes be as clear as mud. Accordingly, unless type coercion is explicitly desired, it’s typically best to use === and !== (rather than == and !=) to avoid any unintended side effects of type coercion. (== and != automatically perform type conversion when comparing two things, whereas === and !== do the same comparison without type conversion.)

Since we’re talking about type coercion and comparisons, it’s worth mentioning that comparing NaN with anything (even NaN!) will always return false. You therefore cannot use the equality operators (==, ===, !=, !==) to determine whether a value is NaN or not. Instead, use the built-in global isNaN() function:

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

JavaScript Issue No. 5: Inefficient DOM Manipulation

JavaScript makes it relatively easy to manipulate the DOM (i.e., add, modify, and remove elements), but does nothing to promote doing so efficiently.

A common example is code that adds a series of DOM elements one at a time. Adding a DOM element is an expensive operation, and code that adds multiple DOM elements consecutively is inefficient and likely not to work well.

One effective alternative when multiple DOM elements need to be added is to use document fragments instead, which will improve efficiency and performance.

For example:

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));

In addition to the inherently improved efficiency of this approach, creating attached DOM elements is expensive, whereas creating and modifying them while detached and then attaching them yields much better performance.

JavaScript Issue No. 6: Incorrect Use of Function Definitions Inside for Loops

Consider this code:

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);
    };
}

Based on the above code, if there were 10 input elements, clicking any of them would display “This is element #10”! This is because, by the time onclick is invoked for any of the elements, the above for loop will have completed and the value of i will already be 10 (for all of them).

Here’s how we can correct this JavaScript problem to achieve the desired behavior:

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 version of the code, makeHandler is immediately executed each time we pass through the loop, each time receiving the then-current value of i+1 and binding it to a scoped num variable. The outer function returns the inner function (which also uses this scoped num variable) and the element’s onclick is set to that inner function. This ensures that each onclick receives and uses the proper i value (via the scoped num variable).

JavaScript Issue No. 7: Failure to Properly Leverage Prototypal Inheritance

A surprisingly high number of JavaScript developers fail to fully understand, and therefore fully leverage, the features of prototypal inheritance.

Here’s a simple example:

BaseObject = function(name) {
    if (typeof name !== "undefined") {
        this.name = name;
    } else {
        this.name = 'default'
    }
};

This seems fairly straightforward. If you provide a name, use it, otherwise set the name to ‘default’. For instance:

var firstObj = new BaseObject();
var secondObj = new BaseObject('unique');

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

But what if we were to do this:

delete secondObj.name;

We’d then get:

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

But wouldn’t it be nicer for this to revert to ‘default’? This can easily be done if we modify the original code to leverage prototypal inheritance, as follows:

BaseObject = function (name) {
    if(typeof name !== "undefined") {
        this.name = name;
    }
};

BaseObject.prototype.name = 'default';

With this version, BaseObject inherits the name property from its prototype object, where it is set (by default) to 'default'. Thus, if the constructor is called without a name, the name will default to default. Similarly, if the name property is removed from an instance of BaseObject, the prototype chain will then be searched and the name property will be retrieved from the prototype object where its value is still 'default'. So now we get:

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 Incorrect References to Instance Methods

Let’s define a simple object, and create an instance of it, as follows:

var MyObjectFactory = function() {}
	
MyObjectFactory.prototype.whoAmI = function() {
    console.log(this);
};

var obj = new MyObjectFactory();

Now, for convenience, let’s create a reference to the whoAmI method, presumably so we can access it merely by whoAmI() rather than the longer obj.whoAmI():

var whoAmI = obj.whoAmI;

And just to be sure we’ve stored a reference to a function, let’s print out the value of our new whoAmI variable:

console.log(whoAmI);

Outputs:

function () {
    console.log(this);
}

It looks fine so far.

But look at the difference when we invoke obj.whoAmI() versus our convenience reference whoAmI():

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

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

Arrow functions ((params) => {} instead of function(params) {} provide a static this that is not based on the calling context like this is for regular functions. This gives us a workaround:

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 may have noticed that, even though we got the output to match, this is a reference to the factory, rather than to the instance. Rather than trying to fix this issue further, it’s worth considering approaches to JavaScript that don’t rely on this (or even new) at all, as explained in As a JS Developer, This Is What Keeps Me Up at Night.

JavaScript Issue No. 9: Providing a String As the First Argument to setTimeout or setInterval

For starters, let’s be clear on something here: Providing a string as the first argument to setTimeout or setInterval is not itself a mistake per se. It is perfectly legitimate JavaScript code. The issue here is more one of performance and efficiency. What is often overlooked is that if you pass in a string as the first argument to setTimeout or setInterval, it will be passed to the function constructor to be converted into a new function. This process can be slow and inefficient, and is rarely necessary.

The alternative to passing a string as the first argument to these methods is to instead pass in a function. Let’s look at an example.

Here, then, would be a fairly typical use of setInterval and setTimeout, passing a string as the first parameter:

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

The better choice would be to pass in a function as the initial argument, e.g.:

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: Failure to Use “Strict Mode”

As explained in our JavaScript Hiring Guide, “strict mode” (i.e., including 'use strict'; at the beginning of your JavaScript source files) is a way to voluntarily enforce stricter parsing and error handling on your JavaScript code at runtime, as well as a way to make your code more secure.

While, admittedly, failing to use strict mode is not truly a “mistake,” its use is increasingly being encouraged and its omission is increasingly considered bad form.

Here are some key benefits of strict mode:

  • Makes debugging easier. Code errors that would otherwise have been ignored or would have failed silently will now generate errors or throw exceptions, alerting you sooner to problems with JavaScript in your code base and directing you more quickly to their source.
  • Prevents accidental globals. Without strict mode, assigning a value to an undeclared variable automatically creates a global variable with that name. This is one of the most common JavaScript errors. In strict mode, attempting to do so throws an error.
  • Eliminates this coercion. Without strict mode, a reference to a this value of null or undefined is automatically coerced to the globalThis variable. This can cause many frustrating bugs. In strict mode, referencing a this value of null or undefined throws an error.
  • Disallows duplicate property names or parameter values. Strict mode throws an error when it detects a duplicate named property in an object (e.g., var object = {foo: "bar", foo: "baz"};) or a duplicate named argument for a function (e.g., function foo(val1, val2, val1){}), thereby catching what is almost certainly a bug in your code that you might otherwise have wasted significant time tracking down.
  • Makes eval() safer. There are some differences in the way eval() behaves in strict mode and in nonstrict mode. Most significantly, in strict mode, variables and functions declared inside an eval() statement are not created in the containing scope. (They are created in the containing scope in nonstrict mode, which can also be a common source of problems with JavaScript.)
  • Throws an error on invalid use of delete. The delete operator (used to remove properties from objects) cannot be used on nonconfigurable properties of the object. Nonstrict code will fail silently when an attempt is made to delete a nonconfigurable property, whereas strict mode will throw an error in such a case.

Mitigating JavaScript Issues With a Smarter Approach

As is true with any technology, the better you understand why and how JavaScript works and doesn’t work, the more solid your code will be and the more you’ll be able to effectively harness the true power of the language. Conversely, a lack of proper understanding of JavaScript paradigms and concepts is where many JavaScript problems lie.Thoroughly familiarizing yourself with the language’s nuances and subtleties is the most effective strategy for improving your proficiency and increasing your productivity.

Further Reading on the Toptal Blog:

Understanding the basics

  • What are the common errors in JavaScript?

    The common errors that developers make while coding in JavaScript include mistaken thinking about how the “this” keyword works, incorrect assumptions about block scoping, and a failure to avoid memory leaks. JavaScript’s evolution over time has left many pitfalls if old coding patterns are followed.

  • You can improve your JavaScript skills by putting best practices to use in real code, and reading about the nuances inherent in the language to be aware of its more advanced features and limitations.

  • Buggy code can look perfectly innocent on the surface. By learning about common JavaScript issues, you can start to understand what makes certain coding patterns problematic and how to avoid them in your own code.

  • Some might contend that the JavaScript language itself is problematic. Indeed, it has its shortcomings, but it’s also ubiquitous—so it pays to understand how to navigate them if you (like most of today’s developers) have to work with some form of JavaScript code.

Hire a Toptal expert on this topic.
Hire Now
Ryan J. Peterson

Ryan J. Peterson

Verified Expert in Engineering

Hershey, PA, United States

Member since November 25, 2013

About the author

Ryan is an architect, entrepreneur, and developer. He is skilled in building cloud-scalable, extensible software systems.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

PREVIOUSLY AT

Automation Anywhere

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

Join the Toptal® community.