Skip to content

JavaScript Mistakes

This chapter highlights common pitfalls and misunderstandings that JavaScript developers, especially beginners, might encounter. Awareness of these can save a lot of debugging time.

1. Using Assignment (=) Instead of Comparison (== or ===)

Section titled “1. Using Assignment (=) Instead of Comparison (== or ===)”

Accidentally using the assignment operator (=) where a comparison operator (== for loose equality or === for strict equality) is intended, typically in if statements or loops, is a frequent error.

An assignment expression (x = value) evaluates to value. If value is truthy, the if block executes; if falsy, it doesn’t.

let x = 0;
// Mistake: Assignment instead of comparison
if (x = 10) { // This assigns 10 to x, and 10 is truthy
console.log("This will run, but x is now 10, not 0!");
}
let y = 0;
// Correct: Comparison
if (y === 0) { // This correctly checks if y is equal to 0
console.log("This will run because y is indeed 0.");
}
// Another mistake example
let z = 0;
if (z = 0) { // Assigns 0 to z; 0 is falsy
console.log("This will NOT run.");
}

Best Practice: Always use strict equality (===) or strict inequality (!==) for comparisons to avoid unexpected type coercion issues (see next point).

2. Misunderstanding Loose (==) vs. Strict (===) Comparison

Section titled “2. Misunderstanding Loose (==) vs. Strict (===) Comparison”

JavaScript has two sets of equality operators:

  • == (Loose Equality) and != (Loose Inequality): Perform type coercion if operands are of different types before comparison. This can lead to unintuitive results.
  • === (Strict Equality) and !== (Strict Inequality): Compare both value and type, without type coercion. Generally safer and recommended.

Loose comparison pitfalls:

console.log(10 == "10"); // true (string "10" is coerced to number 10)
console.log(0 == false); // true (false is coerced to 0)
console.log('' == false); // true (empty string coerced to 0, false to 0)
console.log(null == undefined); // true (special case in loose equality)

Strict comparison is more predictable:

console.log(10 === "10"); // false (number vs. string)
console.log(0 === false); // false (number vs. boolean)
console.log('' === false); // false (string vs. boolean)
console.log(null === undefined); // false (different types)

Remember: switch statements use strict comparison (===) for their cases.

let val = 10;
switch (val) {
case "10": // This case will not match because val (number 10) !== "10" (string)
console.log("Matched string 10");
break;
case 10: // This case will match
console.log("Matched number 10");
break;
}

3. Confusing Addition (+) and String Concatenation (+)

Section titled “3. Confusing Addition (+) and String Concatenation (+)”

The + operator is used for both numerical addition and string concatenation. If one of the operands is a string, JavaScript will convert the other operand to a string and perform concatenation.

let num1 = 10;
let num2 = 5;
console.log(num1 + num2); // Output: 15 (numerical addition)
let str1 = "10";
let str2 = "5";
console.log(str1 + str2); // Output: "105" (string concatenation)
console.log(num1 + str2); // Output: "105" (num1 is converted to string "10")

This is especially tricky when dealing with form inputs, which are often strings. Use parseInt(), parseFloat(), or the unary + operator to convert strings to numbers before arithmetic if needed.

let inputVal = "20";
// let sum = inputVal + 10; // "2010"
let sum = Number(inputVal) + 10; // 30 (or parseInt(inputVal) + 10)
console.log(sum);

4. Misunderstanding Floating-Point Precision

Section titled “4. Misunderstanding Floating-Point Precision”

JavaScript numbers are stored as 64-bit IEEE 754 floating-point numbers. This format can lead to precision issues with decimal arithmetic, a common problem in many programming languages.

let x = 0.1;
let y = 0.2;
let z = x + y;
console.log(z); // Output might be 0.30000000000000004, not exactly 0.3
if (z === 0.3) { // This comparison might fail
console.log("z is 0.3");
} else {
console.log("z is NOT exactly 0.3 due to precision: " + z);
}

Solutions:

  • For comparisons, check if the absolute difference is within a small tolerance (epsilon): Math.abs(z - 0.3) < 0.000001.
  • For display, use toFixed(): z.toFixed(2) would give “0.30”.
  • For critical financial calculations, consider using libraries that handle decimal arithmetic with arbitrary precision, or work with integers (e.g., store currency as cents).

5. Incorrectly Breaking Strings Across Lines

Section titled “5. Incorrectly Breaking Strings Across Lines”

Old way (using backslash \ - error-prone and outdated):

// const oldMultiline = "This is a very long \
// string that continues on the next line."; // This works but is not recommended

Modern way (Template Literals - ES6+):

Template literals (using backticks `) are the best way to create multiline strings and embed expressions.

const modernMultiline = `This is a very long
string that naturally
continues on multiple lines.`;
console.log(modernMultiline);

6. Misplacing Semicolons (Automatic Semicolon Insertion - ASI)

Section titled “6. Misplacing Semicolons (Automatic Semicolon Insertion - ASI)”

JavaScript uses Automatic Semicolon Insertion (ASI) to insert semicolons where they are omitted but expected. While convenient, it can lead to unexpected behavior if not understood.

Example: Misplaced semicolon after if condition.

let condition = false;
if (condition); { // Semicolon here ends the if statement prematurely!
// This block will ALWAYS execute, regardless of 'condition'.
console.log("This block runs unexpectedly!");
}

Best Practice: While optional in many places, consistently using semicolons at the end of statements can prevent ASI-related bugs and improve code clarity.

7. Breaking a return Statement Across Lines

Section titled “7. Breaking a return Statement Across Lines”

ASI can cause issues with return statements if the returned value is on a new line without the return keyword.

function getObject() {
return // ASI inserts a semicolon here!
{
name: "Problematic Object"
};
}
console.log(getObject()); // Output: undefined, not the object

Because ASI inserts a semicolon after return, the function effectively becomes return;, thus returning undefined. The object literal below it is never reached as part of the return value.

Correct way:

function getCorrectObject() {
return {
name: "Correct Object"
};
}
// Or: return (
// {
// name: "Correct Object"
// }
// );
console.log(getCorrectObject().name); // Output: Correct Object

Keep the opening brace or parenthesis of the returned value on the same line as return.

8. Accessing Arrays with Named Indexes (String Keys)

Section titled “8. Accessing Arrays with Named Indexes (String Keys)”

JavaScript arrays are numerically indexed (0, 1, 2, …). Using string keys to set values on an array doesn’t add elements to the array; it sets properties on the array object.

let myArray = [];
myArray[0] = "Apple";
myArray["fruitName"] = "Banana"; // This sets a property, not an array element
console.log(myArray.length); // Output: 1 (only "Apple" is an element)
console.log(myArray[0]); // Output: "Apple"
console.log(myArray.fruitName); // Output: "Banana" (accessed as a property)
console.log(myArray[1]); // Output: undefined
// Array methods like forEach will ignore 'fruitName'
myArray.forEach(item => console.log(item)); // Logs only "Apple"

For key-value pairs, use plain Objects ({}) or Map objects.

const myMap = new Map();
myMap.set("fruitName", "Cherry");
console.log(myMap.get("fruitName")); // Output: Cherry

9. Trailing Commas in Array and Object Literals

Section titled “9. Trailing Commas in Array and Object Literals”

Historically, trailing commas (a comma after the last element/property) were problematic in some older JavaScript engines (especially IE).

// Older problematic example for some engines:
// const points = [40, 100, 1, ];
// const person = { firstName: "John", lastName: "Doe", age: 46, };

Modern JavaScript (ES5 and later) fully supports trailing commas in array and object literals. They are now considered good practice because they lead to cleaner version control diffs (adding a new item only changes one line).

// Modern and good practice:
const validPoints = [
40,
100,
1, // Trailing comma is fine
];
const validPerson = {
firstName: "John",
lastName: "Doe",
age: 46, // Trailing comma is fine
};

Trailing commas are also allowed in function parameter lists and call arguments in ES2017+.

undefined and null are two distinct primitive types in JavaScript, though they are sometimes used interchangeably or confused.

  • undefined: Typically means a variable has been declared but not yet assigned a value, or an object property/array element that doesn’t exist. It’s JavaScript’s default value for uninitialized variables.
  • null: Represents the intentional absence of any object value. It must be explicitly assigned by a programmer.
let uninitializedVar;
console.log(uninitializedVar); // undefined
let emptyObject = null;
console.log(emptyObject); // null
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (this is a historical quirk of JavaScript)
console.log(null == undefined); // true (loose equality)
console.log(null === undefined); // false (strict equality)

When checking if an object or variable might be uninitialized or explicitly null, it’s often better to check for both, or be specific based on context:

let myObj = null; // or could be undefined
// To check if myObj has a value (is not null or undefined):
if (myObj != null) { // Loose check for both null and undefined
// ...
}
// To check specifically for null:
if (myObj === null) {
// ...
}
// To check if an object property exists, use `in` operator or `hasOwnProperty` or check against `undefined`.
const data = {};
if (data.property === undefined) { /* ... */ }

11. Misunderstanding Variable Scope (var vs. let/const)

Section titled “11. Misunderstanding Variable Scope (var vs. let/const)”

Before ES6, var was the only way to declare variables. var has function scope or global scope, not block scope. This means a variable declared with var inside a block (e.g., an if statement or for loop) is accessible outside that block, within its containing function.

function scopeTestVar() {
if (true) {
var x = 10; // 'x' is function-scoped
}
console.log(x); // Output: 10 (x is accessible here)
for (var i = 0; i < 3; i++) {
// 'i' is function-scoped
}
console.log(i); // Output: 3 (i is accessible here and its value is 3 after loop)
return i;
}
scopeTestVar();

ES6 introduced let and const, which have block scope. This means they are only accessible within the block ({...}) where they are defined. This is generally more intuitive and helps prevent bugs.

function scopeTestLet() {
if (true) {
let y = 20; // 'y' is block-scoped
console.log(y); // Output: 20
}
// console.log(y); // Error: y is not defined here
for (let j = 0; j < 3; j++) {
// 'j' is block-scoped for each iteration
console.log(j); // 0, 1, 2
}
// console.log(j); // Error: j is not defined here
// return j; // This would also be an error
}
scopeTestLet();

Best Practice: Prefer let and const over var in modern JavaScript to leverage block scoping and avoid common var-related issues like unintentional variable hoisting or reassignment.