Common JavaScript bugs and gotchas — a field reference

Every bug in this lesson has already been explained mechanically somewhere earlier in this domain — this is the field-reference version, the shape each one actually takes in real code, so it's recognizable on sight instead of requiring the mechanism to be re-derived from scratch every time.

Advanced

3 min read

Bug 1: the var-loop closure that captures the wrong value

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // prints 3, 3, 3 — not 0, 1, 2
}

Covered mechanically in the scope-and-closures lesson: var has no block scope, so every closure shares the exact same i, already at its final value by the time any callback runs. The fix: use let, which creates a genuinely fresh binding per iteration.

Bug 2: a detached method losing its this

class Timer { tick() { this.seconds++; } }
const timer = new Timer();
setInterval(timer.tick, 1000); // `this` inside tick is NOT timer

Covered mechanically in the this lesson: this is resolved at the call site, not the definition site — passing a method as a bare callback detaches it from its intended object. The fix: an arrow function defined in the constructor (this.tick = () => {...}), or .bind(timer).

Bug 3: assuming fetch rejects on an HTTP error status

try {
  const res = await fetch("/api/users/999"); // returns 404 — this does NOT throw
  const data = await res.json();
} catch (err) { /* never reached for a 404 */ }

Covered mechanically in the fetch lesson: fetch's Promise only rejects for genuine network failures, never for an HTTP error status code — a 404 or 500 is still a "successful" fetch as far as the Promise is concerned. The fix: explicitly check response.ok and throw if it's false.

Bug 4: an unhandled promise rejection from a forgotten await

async function saveData() { throw new Error("failed"); }
saveData(); // called without await or .catch() — the rejection has nowhere to go

Covered mechanically in the error-handling lesson: an async function's thrown error becomes a rejected Promise, not a synchronous exception a normal try/catch around the call site would catch. The fix: always await (inside a try/catch) or attach .catch() to any async call whose failure actually matters.

Bug 5: == producing a surprising true/false

"" == "0";  // false
0 == "";    // true — inconsistent-LOOKING results from the same "empty-ish" comparison

Covered mechanically in the type-coercion lesson: =='s coercion algorithm is real and learnable but genuinely non-obvious, and its results aren't simply transitive the way intuition suggests. The fix: use ===, which skips coercion entirely — different types are immediately false, with no algorithm to reason through.

Bug 6: a memory leak from a timer or listener that's never cleaned up

class Widget {
  constructor() { this.id = setInterval(() => this.refresh(), 1000); }
  // no destroy() method clearing the interval — `this` leaks for the page's remaining lifetime
}

Covered mechanically in the memory-leaks lesson: a timer's closure holds a reference to this, keeping the entire object reachable (and un-collectible) for as long as the timer runs. The fix: always pair setup (setInterval, addEventListener) with matching cleanup (clearInterval, removeEventListener) in a component's teardown path.

The actual throughline across all six

Every one of these traces back to the same handful of mechanisms this domain already covered in depth: closures capturing references rather than value snapshots, this being resolved by call site rather than definition site, and Promises having their own distinct success/failure semantics that don't automatically map onto every intuition carried over from synchronous code. Recognizing a bug's shape on sight — "this smells like a detached this," "this smells like an unhandled rejection" — is what separates fixing a JavaScript bug quickly from re-deriving these mechanisms from first principles every single time one shows up.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why does `setInterval(timer.tick, 1000)` fail to work correctly, losing access to timer's own data?

2. Why does a try/catch around a fetch() call fail to catch a 404 response?

3. What's the actual throughline connecting all six bugs in this field-reference lesson?