Marcell Ciszek Druzynski

JS mistakes

Common JavaScript Mistakes to Avoid

November 22, 2024

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!

The for in problem

Looping using the for in loop is different compare to the for of loop, and it is common to mix those. Let’s say we want to loop over an array of names and print them to the console, can we 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 guessed that it will print out the names it’s wrong! We are getting 0,1,2 printed to the console. The question is why? We need to understand how the for in loop works. The for in loop to not loop over the values but instead loop through the keys in an object. Since arrays in javaScript are plain object we bailly get the indexes and not the values. To solve the problem we could just change to using the for of loop instead.

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.

Overusing Optional Chaining

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, I believe 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.

Let’s look at an example:

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
9type Data = typeof data;
10function getThemesWithOptionalChaing(data: Data) {
11 let res = [];
12 for (let user of data.users) {
13 res.push(user.profile?.preferences?.theme);
14 }
15 return res;
16}
17getThemesWithOptionalChaing(data); // [dark, undefined, undefined]
18
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
9type Data = typeof data;
10function getThemesWithOptionalChaing(data: Data) {
11 let res = [];
12 for (let user of data.users) {
13 res.push(user.profile?.preferences?.theme);
14 }
15 return res;
16}
17getThemesWithOptionalChaing(data); // [dark, undefined, undefined]
18

Using optional chaining, we would get [dark, undefined, undefined].

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.

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
9type Data = typeof data;
10function getThemes(data: Data) {
11 let res = [];
12 for (let user of data.users) {
13 if (user.profile && user.profile.preferences) {
14 res.push(user.profile.preferences.theme);
15 } else {
16 console.log("Missing profile or preferences");
17 }
18 }
19 return res;
20}
21getThemes(data); // [dark]
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
9type Data = typeof data;
10function getThemes(data: Data) {
11 let res = [];
12 for (let user of data.users) {
13 if (user.profile && user.profile.preferences) {
14 res.push(user.profile.preferences.theme);
15 } else {
16 console.log("Missing profile or preferences");
17 }
18 }
19 return res;
20}
21getThemes(data); // [dark]

Immutable objects

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.

Here, I'd like to focus on how Object.freeze() operates. This useful API allows us to create immutable values in JavaScript.

However, it's important to note that Object.freeze() only applies to objects that are one level deep. When you begin to add 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
14data.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
14data.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.

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

What occurs if we attempt to change the name property within the user object instead?

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
14data.user.name = "Lionel Messi";
15console.log(data.user.name); // Output: Lionel Messi
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
14data.user.name = "Lionel Messi";
15console.log(data.user.name); // Output: Lionel Messi

As you can see, we have successfully mutated the user's name.

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

This keyword in arrow functions

I believe the arrow function in JavaScript is often overused. 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.

In this example, I’ll demonstrate a scenario where using an arrow function would lead to unexpected results, and show how switching to the function keyword resolves the issue.

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 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.

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.

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.