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:
| |
Running this code produces the following error:
| |
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:
| |
Alternatively, in modern browsers, you can use the bind() method to provide the correct reference:
| |
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:
| |
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:
| |
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:
| |
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: element → onClick → element → onClick → element…
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:
| |
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():
| |
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:
| |
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:
| |
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:
| |
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:
| |
This appears straightforward: if a name is provided, use it; otherwise, default to ‘default’. For instance:
| |
But consider this scenario:
| |
This would result in:
| |
Wouldn’t it be more elegant for this to revert to ‘default’? We can easily achieve this by leveraging prototypal inheritance:
| |
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:
| |
JavaScript Issue No. 8: Creating Faulty References to Instance Methods
Let’s define a simple object and create an instance:
| |
Now, for convenience, we’ll create a reference to the whoAmI method, intending to access it simply as whoAmI() instead of obj.whoAmI():
| |
To confirm that we’ve indeed stored a function reference, let’s print the value of our new whoAmI variable:
| |
Output:
| |
So far, so good.
But observe the difference when invoking obj.whoAmI() versus our convenient reference whoAmI():
| |
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:
| |
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:
| |
The preferred approach is to pass a function as the initial argument:
| |
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
thiscoercion. In non-strict mode, athisvalue of null or undefined is automatically coerced to theglobalThisvariable, 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 howeval()behaves, most notably preventing variables and functions declared inside aneval()statement from being created in the containing scope (which can cause issues in non-strict mode). - Error on invalid
deleteusage. Thedeleteoperator (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.