Marcell Ciszek Druzynski
← Back to blog posts

Common JavaScript Mistakes to Avoid

Let's dive into some common JavaScript pitfalls—those frustrating moments when something doesn't work as expected, and we need to figure out why.

JavaScript has come a long way since its creation. Over the years, many new features have been added to the language, and some are now preferred over older approaches to help avoid common mistakes. Sometimes, though, it's not about the features but simply that a developer hasn't yet encountered or understood why something behaves the way it does.

So, let's get into some examples and solve these issues together!

1. The for...in vs. for...of confusion

Looping using the for...in loop is different compared to the for...of loop, and it's common to mix these up.

Let's say we want to loop over an array of names and print them to the console. Can you spot the problem here using the for...in loop?

1let names = ["Alice", "Bob", "Eve"];
2
3for (let name in names) {
4 console.log(name);
5}
1let names = ["Alice", "Bob", "Eve"];
2
3for (let name in names) {
4 console.log(name);
5}

If you expected to see the names printed, you'd be surprised! This code actually outputs 0, 1, 2 to the console.

Why does this happen?

We need to understand how the for...in loop works. The for...in loop doesn't iterate over the values but instead iterates through the keys or property names in an object. Since arrays in JavaScript are objects, we get the indexes (which are the keys) and not the values.

Solution: Use for...of for arrays

To solve the problem, we should use the for...of loop instead, which was designed specifically for iterating over iterable objects like arrays:

1let names = ["Alice", "Bob", "Eve"];
2
3for (let name of names) {
4 console.log(name);
5}
1let names = ["Alice", "Bob", "Eve"];
2
3for (let name of names) {
4 console.log(name);
5}

Now we get the expected result: Alice, Bob, Eve.

Alternatively, if you still want to use for...in but need the values, you can access them through the index:

1let names = ["Alice", "Bob", "Eve"];
2
3for (let index in names) {
4 console.log(names[index]);
5}
1let names = ["Alice", "Bob", "Eve"];
2
3for (let index in names) {
4 console.log(names[index]);
5}

2. Using Optional Chaining Wisely

Optional chaining (?.) is an excellent feature that has made our code cleaner and more readable compared to the old approach of using if statements to check if properties exist on an object.

However, it's sometimes used incorrectly. Not all checks or potential errors should be silent. There are cases where it's better to explicitly handle errors or indicate when something is missing.

Example: Retrieving user themes

Suppose we want to iterate through our users' data and retrieve their themes saved as user settings:

1const data = {
2 users: [
3 { id: 1, profile: { preferences: { theme: "dark" } } },
4 { id: 2, profile: null },
5 { id: 3 }, // Completely missing profile
6 ],
7};
8
9function getThemesWithOptionalChaining(data) {
10 let res = [];
11 for (let user of data.users) {
12 res.push(user.profile?.preferences?.theme);
13 }
14 return res;
15}
16
17getThemesWithOptionalChaining(data); // ["dark", undefined, undefined]
1const data = {
2 users: [
3 { id: 1, profile: { preferences: { theme: "dark" } } },
4 { id: 2, profile: null },
5 { id: 3 }, // Completely missing profile
6 ],
7};
8
9function getThemesWithOptionalChaining(data) {
10 let res = [];
11 for (let user of data.users) {
12 res.push(user.profile?.preferences?.theme);
13 }
14 return res;
15}
16
17getThemesWithOptionalChaining(data); // ["dark", undefined, undefined]

Using optional chaining, we silently get ["dark", undefined, undefined].

A better approach: Explicit error handling

Instead, let's check if user.profile.preferences actually exists before adding it to the list. If it doesn't exist, we should at least log an error so developers are aware of the issue:

1function getThemes(data) {
2 let res = [];
3 for (let user of data.users) {
4 if (user.profile && user.profile.preferences) {
5 res.push(user.profile.preferences.theme);
6 } else {
7 console.log(`Missing profile or preferences for user ID: ${user.id}`);
8 // You could also add a default theme here:
9 // res.push("default-theme");
10 }
11 }
12 return res;
13}
14
15getThemes(data); // ["dark"]
1function getThemes(data) {
2 let res = [];
3 for (let user of data.users) {
4 if (user.profile && user.profile.preferences) {
5 res.push(user.profile.preferences.theme);
6 } else {
7 console.log(`Missing profile or preferences for user ID: ${user.id}`);
8 // You could also add a default theme here:
9 // res.push("default-theme");
10 }
11 }
12 return res;
13}
14
15getThemes(data); // ["dark"]

This approach gives us more visibility into what's missing and why, which is particularly helpful during development or when debugging issues in production.

3. Understanding Object Immutability

A common misconception among JavaScript developers is regarding immutability in the language. While it's important to differentiate between var, let, and const—which is a separate topic—it's crucial to remember that const does not make values immutable; it simply prevents reassignment of the variable.

Object.freeze() is a useful API that allows us to create immutable objects in JavaScript. However, it's important to note that Object.freeze() only applies to the direct properties of an object (shallow freeze). When you have nested objects, Object.freeze() does not ensure the immutability of those inner objects.

1let data = Object.freeze({
2 id: 1,
3 type: "user",
4 user: {
5 name: "Zlatan Ibrahimovic",
6 age: 42,
7 address: {
8 street: "123 Main St",
9 city: "New York",
10 },
11 },
12});
13
14// This will fail as expected
15data.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.
1let data = Object.freeze({
2 id: 1,
3 type: "user",
4 user: {
5 name: "Zlatan Ibrahimovic",
6 age: 42,
7 address: {
8 street: "123 Main St",
9 city: "New York",
10 },
11 },
12});
13
14// This will fail as expected
15data.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.

The id property on the data object cannot be changed, as expected.

But what happens if we try to modify the name property within the user object?

1data.user.name = "Lionel Messi";
2console.log(data.user.name); // Output: Lionel Messi
1data.user.name = "Lionel Messi";
2console.log(data.user.name); // Output: Lionel Messi

As you can see, we have successfully mutated the user's name, because Object.freeze() doesn't freeze nested objects.

Solution: Deep Freezing

If you need true immutability with nested objects, you'll need to implement a "deep freeze" function:

1function deepFreeze(obj) {
2 // Freeze the object first
3 Object.freeze(obj);
4
5 // Get all property names of the object
6 const propNames = Object.getOwnPropertyNames(obj);
7
8 // Freeze each property if it's an object
9 for (const name of propNames) {
10 const value = obj[name];
11
12 if (value && typeof value === "object" && !Object.isFrozen(value)) {
13 deepFreeze(value);
14 }
15 }
16
17 return obj;
18}
19
20const data = deepFreeze({
21 id: 1,
22 user: {
23 name: "Zlatan Ibrahimovic",
24 },
25});
26
27// Now this will also fail
28try {
29 data.user.name = "Lionel Messi";
30} catch (e) {
31 console.log("Can't modify nested objects either!"); // This will execute
32}
1function deepFreeze(obj) {
2 // Freeze the object first
3 Object.freeze(obj);
4
5 // Get all property names of the object
6 const propNames = Object.getOwnPropertyNames(obj);
7
8 // Freeze each property if it's an object
9 for (const name of propNames) {
10 const value = obj[name];
11
12 if (value && typeof value === "object" && !Object.isFrozen(value)) {
13 deepFreeze(value);
14 }
15 }
16
17 return obj;
18}
19
20const data = deepFreeze({
21 id: 1,
22 user: {
23 name: "Zlatan Ibrahimovic",
24 },
25});
26
27// Now this will also fail
28try {
29 data.user.name = "Lionel Messi";
30} catch (e) {
31 console.log("Can't modify nested objects either!"); // This will execute
32}

Be cautious and aware of these types of issues when using Object.freeze() in your programs.

4. The this keyword in arrow functions vs. regular functions

Arrow functions in JavaScript are very useful, but they're sometimes used inappropriately. While choosing between the function keyword and the () => syntax often comes down to design principles and personal preference, it's crucial to understand the key differences and potential pitfalls.

One significant difference is how this behaves in arrow functions versus regular functions.

1let data = {
2 id: 1,
3 type: "user",
4 user: {
5 name: "Zlatan Ibrahimovic",
6 age: 42,
7 address: {
8 street: "123 Main St",
9 city: "New York",
10 },
11 greet: () => {
12 console.log(`Hello, I am ${this.name}`);
13 },
14 },
15};
16
17data.user.name = "Lionel Messi";
18
19data.user.greet(); // Hello, I am undefined
1let data = {
2 id: 1,
3 type: "user",
4 user: {
5 name: "Zlatan Ibrahimovic",
6 age: 42,
7 address: {
8 street: "123 Main St",
9 city: "New York",
10 },
11 greet: () => {
12 console.log(`Hello, I am ${this.name}`);
13 },
14 },
15};
16
17data.user.name = "Lionel Messi";
18
19data.user.greet(); // Hello, I am undefined

We get undefined when trying to access the name. Why does this happen?

The issue: Arrow functions and this

The issue lies in the greet method, which incorrectly uses the this keyword within an arrow function. Arrow functions don't have their own this context; instead, they inherit this from the surrounding lexical scope. In this case, this does not refer to the user object as intended but to the global scope (or undefined in strict mode).

Solution: Use regular functions for methods

To fix this, we should use a regular function for the greet method instead of an arrow function:

1let data = {
2 id: 1,
3 type: "user",
4 user: {
5 name: "Zlatan Ibrahimovic",
6 age: 42,
7 address: {
8 street: "123 Main St",
9 city: "New York",
10 },
11 greet() {
12 console.log(`Hello, I am ${this.name}`);
13 },
14 },
15};
16
17data.user.name = "Lionel Messi";
18
19data.user.greet(); // Hello, I am Lionel Messi
1let data = {
2 id: 1,
3 type: "user",
4 user: {
5 name: "Zlatan Ibrahimovic",
6 age: 42,
7 address: {
8 street: "123 Main St",
9 city: "New York",
10 },
11 greet() {
12 console.log(`Hello, I am ${this.name}`);
13 },
14 },
15};
16
17data.user.name = "Lionel Messi";
18
19data.user.greet(); // Hello, I am Lionel Messi

Now, greet is a regular function, and it correctly refers to the user object using this.

When to use arrow functions

Arrow functions are excellent for:

1// Good use of arrow function in a callback
2const numbers = [1, 2, 3, 4];
3const doubled = numbers.map((num) => num * 2);
1// Good use of arrow function in a callback
2const numbers = [1, 2, 3, 4];
3const doubled = numbers.map((num) => num * 2);

5. Equality comparisons in JavaScript

Another common source of bugs in JavaScript is misunderstanding equality comparisons. JavaScript has two types of equality operators: loose equality (==) and strict equality (===).

1// Loose equality with type coercion
2console.log(0 == "0"); // true
3console.log(0 == false); // true
4console.log("" == false); // true
5
6// Strict equality without type coercion
7console.log(0 === "0"); // false
8console.log(0 === false); // false
9console.log("" === false); // false
1// Loose equality with type coercion
2console.log(0 == "0"); // true
3console.log(0 == false); // true
4console.log("" == false); // true
5
6// Strict equality without type coercion
7console.log(0 === "0"); // false
8console.log(0 === false); // false
9console.log("" === false); // false

Best practice: Use strict equality

As a best practice, you should almost always use strict equality (===) in JavaScript to avoid unexpected behavior due to type coercion. Strict equality compares both value and type, making your code more predictable:

1// Clear intent with strict equality
2if (userId === 123) {
3 // We know userId is exactly the number 123
4}
5
6// Potential bugs with loose equality
7if (userId == "123") {
8 // This condition would be true if userId is the number 123 or the string "123"
9}
1// Clear intent with strict equality
2if (userId === 123) {
3 // We know userId is exactly the number 123
4}
5
6// Potential bugs with loose equality
7if (userId == "123") {
8 // This condition would be true if userId is the number 123 or the string "123"
9}

Conclusion

JavaScript is a versatile and powerful language, but it's easy to make mistakes if you're not aware of the language's nuances and best practices. By understanding these common pitfalls and how to avoid them, you can write cleaner, more efficient code and become a more proficient JavaScript developer.

Key takeaways:

Happy coding!