Common Mistakes & Gotchas
JavaScript is forgiving by design — it rarely crashes when it should, which means mistakes hide as wrong results instead of loud errors. The same traps catch beginners and experienced developers alike: silent coercion, a lost this, floating-point arithmetic, and shared mutable state. This page walks through the classic gotchas in a problem → fix format so you can recognize them on sight and reach for the correct idiom automatically.
Loose equality and coercion
Problem: == coerces its operands, producing results that defy intuition.
console.log(0 == ""); // true
console.log(0 == "0"); // true
console.log("" == "0"); // false
console.log([] == false);// true
Fix: Always use ===. The only acceptable loose check is value == null, which matches both null and undefined. Convert types explicitly with Number() or String() before comparing.
Losing this
Problem: Detaching a method from its object drops its receiver, so this becomes undefined (strict) or the global object.
const counter = {
count: 0,
increment() { this.count++; },
};
const inc = counter.increment;
// inc(); // TypeError: Cannot read properties of undefined
Fix: Bind the method (counter.increment.bind(counter)), wrap it in an arrow (() => counter.increment()), or define it as an arrow-function class field so this is captured lexically.
var hoisting and loop captures
Problem: var is function-scoped and hoisted, so closures created in a loop all share one binding.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}
Fix: Use let, which creates a fresh binding per iteration, printing 0, 1, 2. More broadly, prefer const/let and never use var.
Floating-point money math
Problem: Numbers are IEEE-754 doubles, so decimal fractions are inexact.
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
Fix: Compare with a tolerance (Math.abs(a - b) < Number.EPSILON), and for money work in integer cents (1099 rather than 10.99) or use a decimal library.
Mutating shared state
Problem: Objects and arrays are passed by reference, so mutating an argument changes the caller’s data.
function addTax(cart) {
cart.total *= 1.2; // mutates the original
return cart;
}
Fix: Return a new object instead of mutating: return { ...cart, total: cart.total * 1.2 }. Treat inputs as read-only.
Forgetting await
Problem: Calling an async function without await gives you a pending promise, not the value — and errors vanish.
async function save() { /* ... */ }
const result = save(); // a Promise, not the result
console.log(result.id); // undefined
Fix: await the call inside an async function (const result = await save()), and wrap it in try/catch so rejections are handled rather than swallowed.
Mixing up || and ??
Problem: || treats every falsy value (0, "", false) as “missing,” overriding legitimate input.
const quantity = input || 1; // 0 becomes 1 — wrong!
Fix: Use ??, which only falls back on null/undefined: const quantity = input ?? 1 correctly keeps 0.
Mutating an array while iterating
Problem: Removing items during a forward loop skips elements because indices shift.
const nums = [1, 2, 3, 4];
for (let i = 0; i < nums.length; i++) {
if (nums[i] % 2 === 0) nums.splice(i, 1); // skips elements
}
Fix: Build a new array with filter (nums.filter((n) => n % 2 !== 0)), or iterate backwards if you must mutate in place.
Leaking event listeners and timers
Problem: Listeners and intervals attached but never removed keep their closures — and the DOM nodes they reference — alive, leaking memory.
Fix: Pair every addEventListener with removeEventListener (or use { once: true }), and clear timers with clearTimeout/clearInterval when the work or component is done.
A quick reference
| Gotcha | Symptom | Fix |
|---|---|---|
== coercion | Surprising equality | Use === |
Lost this | undefined errors | Bind or arrow |
var in loops | Same value captured | Use let |
| Float math | 0.1 + 0.2 !== 0.3 | Epsilon / integer cents |
| ` | ` defaults | |
| Floating promises | undefined, lost errors | await + try/catch |
Best Practices
- Adopt strict equality and explicit conversion to eliminate coercion surprises.
- Preserve
thiswith arrow functions orbindwhenever you pass methods around. - Replace every
varwithconst/letto avoid hoisting and loop-capture bugs. - Handle money as integer cents and compare floats with a tolerance.
- Treat function arguments as immutable; return new data instead of mutating.
- Never leave a promise unawaited or unhandled, and always clean up listeners and timers.