DEV Community

Srdan Borović
Srdan Borović

Posted on

Common JavaScript Mistakes Every Beginner Makes (With Fixes)

JavaScript has a strange way of being polite about your mistakes. Other languages stop and yell at you. JavaScript nods, keeps running, and hands you undefined three files later.

I learned this on a small score tracker, one of my first real projects. The leaderboard put 100 ahead of 9. No error, no warning. I spent an hour blaming my data before I found the real culprit (see mistake #9).

Most beginner bugs start with a wrong assumption about how the language behaves, so a syntax checker won't save you. Here are ten that catch nearly everyone, along with the fix for each.

1. Declaring variables with var

Older tutorials still teach var, and it ignores block boundaries. A var inside a loop belongs to the whole function, so every iteration shares one variable.

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 3, 3, 3
Enter fullscreen mode Exit fullscreen mode

By the time the timers fire, the loop has finished and i sits at 3.

The fix: Swap var for let, and each iteration gets its own binding. The code above then prints 0, 1, 2.

Make const your default and use let only when a value has to change. Touching a let or const variable before its declaration throws a ReferenceError, while var quietly gives you undefined.

2. Comparing values with ==

The double equals converts types before it compares them, and the results look like a magic trick gone wrong:

"" == 0        // true
false == "0"   // true
[] == ![]      // true
Enter fullscreen mode Exit fullscreen mode

Now picture a permissions check with if (level == 0). An empty string or a stray false slips right through, and your check treats it as level 0.

The fix: Use === and !==, which skip conversion, so different types never match.

The same habit pays off when other people read your code, since a triple equals tells them exactly what you meant. The one common exception is value == null, which some teams allow because it checks for both null and undefined in a single expression.

3. Expecting decimals to add up

Open your browser console and try this:

0.1 + 0.2 // 0.30000000000000004
Enter fullscreen mode Exit fullscreen mode

JavaScript hasn't lost its mind. Numbers live in binary floating point, and 0.1 repeats forever in binary, much like 1/3 repeats forever in decimal. The engine rounds it off, and the tiny errors add up. Python and many other languages behave the same way.

This gets expensive once money enters the picture, because 0.1 + 0.2 === 0.3 returns false.

The fix: Store money as whole cents and format it at the very end.

const totalCents = 10 + 20;
const display = (totalCents / 100).toFixed(2); // "0.30"
Enter fullscreen mode Exit fullscreen mode

For heavier financial work, reach for a library like Decimal.js.

4. Reading properties of undefined

Sooner or later, you'll meet the most famous error message in JavaScript: TypeError: Cannot read properties of undefined. You asked for a property on something that doesn't exist.

return customer.profile.location.zip;
Enter fullscreen mode Exit fullscreen mode

If profile is missing, the whole chain falls over like a row of dominoes.

The fix: Combine optional chaining with nullish coalescing.

return customer?.profile?.location?.zip ?? "Unknown";
Enter fullscreen mode Exit fullscreen mode

The ?. stops and returns undefined as soon as it hits a missing value. The ?? then supplies a fallback. Pick ?? over || here, since || also throws away valid values like 0 and empty strings.

5. Using async callbacks inside forEach

This one looks perfectly reasonable, which makes it sneaky:

records.forEach(async (record) => {
  await database.save(record);
});
console.log("All saved!"); // Runs before the saves finish
Enter fullscreen mode Exit fullscreen mode

forEach calls your function and ignores whatever comes back, including the promise, so nothing waits for the saves.

The fix: Pick the tool that matches your goal. Use for...of when each step depends on the previous one, such as creating an order before its invoice:

for (const record of records) {
  await database.save(record);
}
Enter fullscreen mode Exit fullscreen mode

Use Promise.all for independent tasks, since running them concurrently usually finishes much sooner:

await Promise.all(records.map((record) => database.save(record)));
Enter fullscreen mode Exit fullscreen mode

Remember that a single failed promise rejects the entire Promise.all, so wrap it in a try...catch when failure matters.

6. Assuming fetch fails on a 404

A try...catch around fetch feels like full protection. Sadly, a 404 or 500 won't trigger it. The promise rejects only when the request itself fails, such as a dropped connection, a DNS problem, or a blocked CORS request.

The fix: Check response.ok yourself before you touch the body.

const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
  throw new Error(`Request failed: ${response.status}`);
}
const data = await response.json();
Enter fullscreen mode Exit fullscreen mode

Without this check, your code may try to parse an HTML error page as JSON and crash with a confusing message that points nowhere near the actual problem. Worse, an API that returns its error as valid JSON lets your application carry on and display that error object as though it were user data.

7. Writing object methods as arrow functions

The value of this is a classic source of confusion. Regular functions get this from how you call them. Arrow functions have no this of their own and borrow it from the surrounding scope.

const tracker = {
  events: 0,
  record: () => {
    this.events++; // this is not tracker
  },
};
Enter fullscreen mode Exit fullscreen mode

An object literal doesn't create a scope, so the arrow function looks straight past tracker.

The fix: Use method shorthand for the method, and save arrow functions for callbacks inside it.

const tracker = {
  events: 0,
  record() {
    setTimeout(() => this.events++, 500);
  },
};
Enter fullscreen mode Exit fullscreen mode

The arrow function inherits this from record().

A close cousin of this bug appears when you detach a method from its object. Writing const trigger = tracker.record and then calling trigger() removes the object from the call, so this becomes undefined in strict code. A wrapper like () => tracker.record() preserves the connection.

8. Thinking the spread operator makes a full copy

Spread copies only the top level. The original and the "copy" still share anything nested.

const defaults = { security: { rateLimit: 100 } };
const staging = { ...defaults };

staging.security.rateLimit = 20;
console.log(defaults.security.rateLimit); // 20
Enter fullscreen mode Exit fullscreen mode

You changed one config and broke another.

The fix: Use structuredClone() for a true deep copy.

const staging = structuredClone(defaults);
Enter fullscreen mode Exit fullscreen mode

The old JSON.parse(JSON.stringify(obj)) trick turns dates into strings and drops undefined values, so you can retire it. One caveat: structuredClone() throws an error on objects that contain functions.

9. Sorting numbers without a comparator

Here's the bug behind my leaderboard:

const scores = [10, 9, 100, 2, 25];
scores.sort();
// [10, 100, 2, 25, 9]
Enter fullscreen mode Exit fullscreen mode

By default, sort() converts everything to strings and compares them alphabetically. The string "100" comes before "9" for the same reason "apple" comes before "banana." On top of that, sort() changes the original array.

The fix: Pass a comparator, and use toSorted() to leave the original alone.

const ordered = scores.toSorted((a, b) => a - b);
// [2, 9, 10, 25, 100]
Enter fullscreen mode Exit fullscreen mode

10. Calling an event handler instead of passing it

One pair of parentheses can wreck your button:

button.addEventListener("click", handleClick());
Enter fullscreen mode Exit fullscreen mode

The parentheses run handleClick as the page loads. The listener then receives the return value, usually undefined, so clicks do nothing.

The fix: Pass the function itself. If you need to send arguments, wrap the call in an arrow function.

button.addEventListener("click", handleClick);
button.addEventListener("click", (event) => handleClick(event, userId));
Enter fullscreen mode Exit fullscreen mode

Let your tools catch them first

You don't need to memorize all ten. ESLint's no-var and eqeqeq rules flag mistakes #1 and #2 as you type. TypeScript goes further and warns you about missing properties before your code ever runs. Writing in ES modules turns on strict mode too, so an undeclared variable throws an error instead of becoming a global. Practice does the rest and Mimo's JavaScript course is a great way to learn these concepts through hands-on lessons.

Keep your browser console open and break things on purpose. Change a let back to var. Remove a comparator. Watch what happens. Every bug you cause on a quiet afternoon is one you'll spot faster at 11 p.m. with a deadline breathing down your neck.

Top comments (0)