Skip to content
JavaScript best practices 4 min read

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

GotchaSymptomFix
== coercionSurprising equalityUse ===
Lost thisundefined errorsBind or arrow
var in loopsSame value capturedUse let
Float math0.1 + 0.2 !== 0.3Epsilon / integer cents
`` defaults
Floating promisesundefined, lost errorsawait + try/catch

Best Practices

  • Adopt strict equality and explicit conversion to eliminate coercion surprises.
  • Preserve this with arrow functions or bind whenever you pass methods around.
  • Replace every var with const/let to 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.
Last updated June 1, 2026
Was this helpful?