Back to Notes

JS Closures & Async

Closures

A function that retains access to its enclosing scope's variables even after that outer function has returned.

function makeCounter() {
  let count = 0;             // private — inaccessible from outside
  return { increment: () => ++count, value: () => count };
}
const counter = makeCounter();
counter.increment(); counter.increment();
counter.value();     // 2

Use cases: data privacy, function factories, memoization, event handlers, partial application.

The Loop + Closure Gotcha

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 3, 3, 3 — all callbacks share the SAME function-scoped `i`

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 0, 1, 2 — let creates a NEW binding per iteration

let is block-scoped, so each loop iteration gets its own distinct binding — the var version has exactly one i shared by every closure, which by the time any setTimeout fires has already reached its final value.

this, bind/call/apply

this is resolved at call time, not definition time — except arrow functions, which capture this lexically from their enclosing scope and can never be overridden by .call()/.apply()/.bind().

Call stylethis
obj.method()obj
standalone fn()undefined (strict mode)
new Fn()the newly constructed object
arrow functioninherited from lexical (enclosing) scope
const user = { name: "Alice" };
function greet(greeting) { return `${greeting}, ${this.name}`; }

greet.call(user, "Hello");    // runs immediately, args passed individually
greet.apply(user, ["Hello"]); // runs immediately, args passed as an array
const bound = greet.bind(user);  // returns a NEW function, does not run yet
bound("Hi");

Arrow functions as class methods are the standard fix for the classic "this is undefined in an event handler" bug — since an arrow function field captures the instance's this once at construction, it stays correct regardless of how the method is later invoked (e.g., passed as a bare callback to addEventListener).

Promises & Async/Await

Three states: pendingfulfilled / rejected (immutable once settled — a promise can never change state again after settling).

// Sequential — slow, each waits for the previous to finish first
const a = await fetchA();
const b = await fetchB();

// Parallel — fast, both start immediately
const [a, b] = await Promise.all([fetchA(), fetchB()]);
MethodResolves whenRejects when
Promise.allall fulfillany rejects (fail-fast)
Promise.allSettledall settle either waynever
Promise.racefirst settles, win or losefirst rejects
Promise.anyfirst fulfillsall reject (AggregateError)
// Never await inside a loop when the calls are independent
for (const id of ids) { await fetch(id); }              // sequential — slow
await Promise.all(ids.map(id => fetch(id)));             // parallel — fast

The Event Loop

Call Stack → Microtask Queue (Promise .then/.catch, queueMicrotask) → Macrotask Queue (setTimeout, I/O, UI events)
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Output: 1, 4, 3, 2

The microtask queue always fully drains before the next macrotask runs — this is why the Promise.then callback ("3") runs before the setTimeout(..., 0) callback ("2"), even though the timer was scheduled first and with a 0ms delay.

Interview Drills

  1. Why does the classic for (var i...) { setTimeout(...) } loop print the same final value for every callback?var is function-scoped, not block-scoped — there's exactly one i shared by every closure created in the loop, and by the time any setTimeout callback actually runs (after the synchronous loop has already finished), i holds its final post-loop value.
  2. Why does a Promise.resolve().then(...) callback run before a setTimeout(..., 0) callback, even with a 0ms delay? — Promise callbacks are microtasks; setTimeout callbacks are macrotasks — the event loop always fully empties the microtask queue before pulling the next macrotask, regardless of the macrotask's specified delay.
  3. Why should await inside a for loop over independent async calls be avoided? — Each await pauses the loop until that specific call resolves before starting the next one, making N independent calls run sequentially (N × latency) instead of concurrently — Promise.all(items.map(...)) starts all calls immediately and waits for them together (roughly max latency, not sum).

Cross-links

[[JS Core Language & Data Types]] · [[React Hooks Deep-Dive]] · [[Asyncio]]