JavaScript Common Mistakes
Learning from common mistakes helps you write better JavaScript code. Here are frequent pitfalls to avoid.
Common Errors
Accidental global variables occur when you assign to a variable without declaring it with const, let, or var. JavaScript doesn't throw an error—it silently creates a global variable, which pollutes the global scope and can cause hard-to-debug conflicts when different parts of your code unknowingly share the same variable. This is especially dangerous in large codebases or when using third-party libraries. Always declare variables with const or let. Using strict mode ('use strict';) helps catch this error by throwing a ReferenceError.
Confusing the assignment operator (=) with the equality comparison operator (==) is a common mistake, especially in conditional statements. Writing if (x = 5) doesn't check if x equals 5—it assigns 5 to x and then evaluates 5 as truthy, so the condition is always true. This bug is hard to spot because the code looks correct at a glance. Always use comparison operators (== or ===) in conditions, and many developers use const to prevent reassignment entirely, making this error impossible.
Misunderstanding the difference between loose equality (==) and strict equality (===) leads to unexpected bugs due to type coercion. The == operator converts types before comparing, so 0 == '' is true, '1' == 1 is true, and null == undefined is true. This behavior is inconsistent and surprising. The === operator checks both value and type without conversion, providing predictable results. Always use === and !== to avoid type coercion bugs. The only exception is checking for null/undefined together: value == null catches both.
Expecting loose comparison behavior causes bugs when you rely on JavaScript's implicit type coercion. For example, comparing 0 == '' returns true, but '' == '0' returns false, which is inconsistent. Loose equality has complex, hard-to-remember rules about how different types are converted. New developers often expect comparisons to work intuitively but get surprised by coercion. This is why strict equality (===) is the recommended default—it works the way you expect without hidden type conversions.
Forgetting to return values from functions is a common mistake that makes functions return undefined. If your function calculates something but doesn't include a return statement, the result is lost. This is especially common with arrow functions—if you use curly braces, you must explicitly return: x => { x * 2 } returns undefined, while x => x * 2 implicitly returns the result. Always check that your functions return the values they calculate, and use the function's return value in your code.
Modifying arrays while iterating over them can cause elements to be skipped or processed multiple times. If you remove an element at index 2 during a for loop, the element that was at index 3 shifts to index 2, but your loop counter has already moved to index 3, so you skip it. Similarly, adding elements during iteration can cause infinite loops. Instead, iterate over a copy of the array (.slice()), collect indices to remove and process them separately, or use filter() to create a new array without the unwanted elements.
// Bad: accidental global
function test() {
x = 5; // Creates global variable!
}
// Good: declare variables
function test() {
let x = 5;
}
// Bad: assignment in condition
if (x = 5) { // Assigns 5 to x!
console.log("Wrong!");
}
// Good: comparison
if (x === 5) {
console.log("Correct!");
}
// Bad: loose equality
console.log(0 == ""); // true (surprising!)
console.log(0 == "0"); // true
console.log("" == "0"); // false (inconsistent)
// Good: strict equality
console.log(0 === ""); // false
console.log(0 === 0); // true
More Common Mistakes
Forgetting that this binding changes in callbacks is one of JavaScript's most notorious pitfalls. When you pass a regular function as a callback to setTimeout, addEventListener, or array methods, that function gets called in a different context, and this no longer refers to your object. Inside the callback, this might be undefined (in strict mode) or the global window object. Solutions include using arrow functions (which don't have their own this), using .bind(this) to lock the this value, or storing this in a variable like const self = this. Modern code prefers arrow functions for callbacks.
Not properly handling asynchronous operations leads to race conditions and bugs. A common mistake is treating async code as if it were synchronous: calling fetch(url) and immediately trying to use the data doesn't work because the data isn't available yet. You must use .then(), async/await, or callbacks to handle the result when it's ready. Another mistake is not handling errors from promises—unhandled promise rejections can crash your app. Always use .catch() or try-catch with async/await.
Mutating objects unintentionally causes hard-to-debug issues, especially when objects are shared between different parts of your code. In JavaScript, objects and arrays are passed by reference, so when you modify an object in one function, those changes affect everywhere that object is used. This breaks expectations and causes surprising behavior. Use techniques like object spreading ({...obj}) or Object.assign() to create copies, or use libraries like Immer for immutable updates. Many modern JavaScript patterns emphasize immutability to avoid these bugs.
Infinite loops occur when a loop's condition never becomes false, causing your program to freeze or crash. Common causes include forgetting to increment the loop counter (while (i < 10) without i++), having a condition that can never be false (while (true)), or making a logical error in the condition. In the browser, infinite loops freeze the page and force users to close the tab. Always verify that your loop has a clear exit condition and that you're making progress toward it with each iteration.
Off-by-one errors in loops happen when you iterate one too many or one too few times, often due to confusion between < and <= or between 0-based and 1-based indexing. JavaScript arrays are 0-indexed (first element is [0]), so iterating from i = 0 to i < array.length is correct, but i <= array.length goes one too far. Similarly, for (let i = 1; i < array.length; i++) skips the first element. These errors can cause undefined values, missed items, or accessing elements outside array bounds.
Not validating user input is a critical security and reliability mistake. Users can enter anything into form fields—malicious scripts, empty values, wrong types, or values outside expected ranges. Trusting user input leads to crashes, incorrect calculations, or security vulnerabilities like XSS attacks. Always validate input: check that required fields aren't empty, that numbers are within acceptable ranges, that email addresses match expected patterns, and that strings don't contain dangerous characters. Validate on both client and server sides—client validation improves UX, but server validation is essential for security.
// Bad: losing this
const obj = {
name: "Test",
method: function() {
setTimeout(function() {
console.log(this.name); // undefined!
}, 1000);
}
};
// Good: arrow function preserves this
const obj2 = {
name: "Test",
method: function() {
setTimeout(() => {
console.log(this.name); // "Test"
}, 1000);
}
};
// Bad: infinite loop
// while (true) { } // Never ends!
// Good: proper condition
let i = 0;
while (i < 10) {
console.log(i);
i++; // Don't forget increment!
}