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 style | this |
|---|---|
obj.method() | obj |
standalone fn() | undefined (strict mode) |
new Fn() | the newly constructed object |
| arrow function | inherited 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: pending → fulfilled / 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()]);
| Method | Resolves when | Rejects when |
|---|---|---|
Promise.all | all fulfill | any rejects (fail-fast) |
Promise.allSettled | all settle either way | never |
Promise.race | first settles, win or lose | first rejects |
Promise.any | first fulfills | all 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
- Why does the classic
for (var i...) { setTimeout(...) }loop print the same final value for every callback? —varis function-scoped, not block-scoped — there's exactly oneishared by every closure created in the loop, and by the time anysetTimeoutcallback actually runs (after the synchronous loop has already finished),iholds its final post-loop value. - Why does a
Promise.resolve().then(...)callback run before asetTimeout(..., 0)callback, even with a 0ms delay? — Promise callbacks are microtasks;setTimeoutcallbacks are macrotasks — the event loop always fully empties the microtask queue before pulling the next macrotask, regardless of the macrotask's specified delay. - Why should
awaitinside aforloop over independent async calls be avoided? — Eachawaitpauses 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]]