DEV Community

Cover image for 5 JavaScript Bugs That Look Correct (But Absolutely Are Not)
Dus Mamud
Dus Mamud

Posted on AI-assisted

5 JavaScript Bugs That Look Correct (But Absolutely Are Not)

Every one of the snippets below looks fine. Clean, short, reasonable. The kind of code you'd nod at in a review and move on.

And every single one is broken.

I've run each of these in Node to confirm — no tricks, no outdated browser quirks. Just JavaScript being JavaScript. Let's go through them one by one: the code, what you'd expect, what actually happens, and the fix.

Bug 1: ['1', '2', '3'].map(parseInt)

This one looks like the most natural code in the world. You've got strings, you want numbers, parseInt converts strings to numbers. What could go wrong?

const numbers = ['1', '2', '3'].map(parseInt);
console.log(numbers);
// You'd expect: [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

What you actually get:

[1, NaN, NaN]
Enter fullscreen mode Exit fullscreen mode

Wait, what? The first one works and the rest explode?

Here's the catch: map doesn't call your function with one argument. It calls it with three — the element, the index, and the whole array. And parseInt doesn't take one argument either. It takes two — the string, and the radix (the number base).

So what really happens is:

parseInt('1', 0)  // radix 0 = "figure it out yourself" → 1 ✓
parseInt('2', 1)  // radix 1 = not a real number base → NaN ✗
parseInt('3', 2)  // "3" isn't a valid digit in base 2 → NaN ✗
Enter fullscreen mode Exit fullscreen mode

The array index silently becomes the number base. Beautiful. Terrible.

The fix — wrap it so parseInt only ever sees what you intend:

const numbers = ['1', '2', '3'].map(n => parseInt(n, 10));
// [1, 2, 3] ✓
Enter fullscreen mode Exit fullscreen mode

The lesson: never pass a multi-argument function directly into map, filter, or forEach. They always sneak extra arguments in.

Bug 2: [10, 9, 80].sort()

Sorting numbers. Can't mess that up, right?

const scores = [10, 9, 80].sort();
console.log(scores);
// You'd expect: [9, 10, 80]
Enter fullscreen mode Exit fullscreen mode

What you actually get:

[10, 80, 9]
Enter fullscreen mode Exit fullscreen mode

JavaScript's default sort() doesn't compare numbers. It converts everything to strings first, then sorts alphabetically. And alphabetically, "10" comes before "80" comes before "9" — because "1" < "8" < "9". It's sorting words, not values.

The fix — tell it how to compare:

const scores = [10, 9, 80].sort((a, b) => a - b);
// [9, 10, 80] ✓
Enter fullscreen mode Exit fullscreen mode

The lesson: sort() without a compare function is almost never what you want for numbers. This one has bitten literally everyone at least once.

Bug 3: 0.1 + 0.2 === 0.3

Quick sanity check. One tenth plus two tenths equals three tenths. First-grade math.

console.log(0.1 + 0.2 === 0.3);
// You'd expect: true
Enter fullscreen mode Exit fullscreen mode

What you actually get:

false
Enter fullscreen mode Exit fullscreen mode

Because:

console.log(0.1 + 0.2);
// 0.30000000000000004
Enter fullscreen mode Exit fullscreen mode

Computers store decimals in binary floating point, and some perfectly innocent-looking decimals — like 0.1 — can't be represented exactly in binary. It's like trying to write 1/3 in decimal: 0.3333... forever. The tiny leftover error accumulates, and your "equal" comparison fails.

This isn't even JavaScript-specific — Python, Java, and C all do the same thing. It's just how floats work.

The fix — don't compare floats directly. Compare with a tiny tolerance:

const EPSILON = 0.000001;
console.log(Math.abs((0.1 + 0.2) - 0.3) < EPSILON);
// true ✓
Enter fullscreen mode Exit fullscreen mode

Or round to the decimals you actually care about before comparing.

The lesson: never trust === with decimal math. If money is involved, work in cents (integers), not dollars (floats).

Bug 4: typeof null === 'object'

This one is a fossil — a bug so old it's basically a feature now.

console.log(typeof null);
// You'd expect: "null"
// What you get: "object"
Enter fullscreen mode Exit fullscreen mode

Why? In the very first version of JavaScript, values were stored with a small "type tag" in front of them. Objects had tag 0. And null was represented as a null pointer — which is all zeros. So null accidentally wore the "object" nametag, and the check has returned 'object' ever since, for almost 30 years.

Fixing it now would break half the internet, so it stays. Forever.

This bites you in real code like this:

function process(data) {
  if (typeof data === 'object') {
    console.log(Object.keys(data)); // 💥 TypeError if data is null
  }
}
process(null);
Enter fullscreen mode Exit fullscreen mode

The fix — always pair the check with an explicit null guard:

if (data !== null && typeof data === 'object') {
  // safe now ✓
}
Enter fullscreen mode Exit fullscreen mode

The lesson: typeof tells you the category, not the value. And null has been lying about its category since 1995.

Bug 5: await inside forEach doesn't wait

This is the sneakiest one on the list, because the await keyword is right there. It looks like it must work.

async function saveAll(items) {
  items.forEach(async (item) => {
    await saveToDatabase(item);
  });
  console.log('All saved!');
}

saveAll([1, 2, 3]);
Enter fullscreen mode Exit fullscreen mode

You'd expect: save 1, save 2, save 3, then print "All saved!"

What actually happens: "All saved!" prints first, while the saves are still running. I confirmed it — the log fires before any item finishes.

Why? forEach doesn't know or care about promises. It calls your async function, gets a promise back, and throws it on the floor. It never awaits anything. Your await inside the callback only pauses that one callback — the loop itself blasts through all items instantly and moves on.

The fix — use a for...of loop, which actually respects await:

async function saveAll(items) {
  for (const item of items) {
    await saveToDatabase(item);
  }
  console.log('All saved!'); // now this really runs last ✓
}
Enter fullscreen mode Exit fullscreen mode

(Need them to run in parallel instead? Use await Promise.all(items.map(...)) — that's the deliberate version of what forEach pretends to do.)

The lesson: forEach + async is a trap. The await inside looks like it controls the loop. It doesn't.

The pattern

Look at all five together and a pattern emerges: every bug comes from JavaScript quietly doing something adjacent to what you asked for. Extra arguments slipped into your function. Strings sorted instead of numbers. Binary approximations instead of decimals. A 30-year-old type tag. A loop that ignores your promises.

The code looks correct because each line is reasonable in isolation. The bug lives in what JavaScript does behind the line.

The best defense isn't memorizing gotchas — it's running the code and looking at the actual output. Every result in this post came from a real Node run, not from memory. When something "should work" but doesn't, console.log the intermediate values. The answer is usually one step away from where you're looking.


Which of these has bitten you before? And what's the worst "looks correct" bug you've shipped? I'm collecting them.

Top comments (0)