Closures
beginnerWhat a closure really is, why loops with var trip people up, and how closures power memoization, once(), and module patterns.
Understand the JavaScript event loop for interviews — call stack, microtask and macrotask queues, and how to predict the output of setTimeout vs Promise questions.
Every senior JavaScript interview eventually gets here, because the event loop explains everything else: why setTimeout(fn, 0) doesn't run immediately, why a promise callback beats a timer, why the UI freezes during a long loop, and why async/await doesn't make code parallel. Interviewers rarely ask "explain the event loop" cold — they hand you a snippet and ask "what does this print?" If you can reason through ordering, you understand the runtime.
JavaScript executes on a single thread with three moving parts:
.then, await continuations), queueMicrotask, MutationObserver.setTimeout, setInterval, I/O, message events, user input.Run one macrotask to completion → drain the entire microtask queue → (browser may render) → take the next macrotask.
The critical detail candidates miss: microtasks are drained completely between macrotasks, and microtasks queued during the drain also run before the next macrotask.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");Output: 1, 4, 3, 2.
Reasoning, step by step:
1 and 4 are synchronous — they run as part of the current macrotask (the script itself)..then callback goes to the microtask queue.3), and only then does the event loop pick up the next macrotask (2).setTimeout(() => console.log("timeout 1"), 0);
Promise.resolve()
.then(() => {
console.log("promise 1");
return Promise.resolve();
})
.then(() => console.log("promise 2"));
setTimeout(() => console.log("timeout 2"), 0);
queueMicrotask(() => console.log("microtask"));Output: promise 1, microtask, promise 2, timeout 1, timeout 2.
Every promise reaction is a separate microtask, so chained .then calls don't run back-to-back if other microtasks are already queued. Both timers wait until the microtask queue is completely empty.
await is syntax over promises: everything after an await becomes a microtask continuation.
async function main() {
console.log("A");
await null; // yields — the rest is queued as a microtask
console.log("B");
}
main();
console.log("C");
// A, C, BThis is why await inside a loop serializes work, and why two async functions don't run "in parallel" — they interleave at await points on one thread. If an interviewer asks how to run tasks concurrently, the answer is to start the promises first and await them together — see N async tasks in parallel and Promise.all.
Because microtasks drain completely, an infinite microtask chain blocks rendering and timers forever:
function loop() {
Promise.resolve().then(loop); // never yields to macrotasks — page freezes
}Swap Promise.resolve().then for setTimeout(loop, 0) and the page stays responsive: macrotasks yield between iterations, letting the browser render. This distinction — microtasks for ordering guarantees, macrotasks for yielding — is the senior-level takeaway interviewers listen for.
In Node.js the macrotask side is split into phases (timers → I/O → setImmediate → close callbacks), and process.nextTick runs before promise microtasks. If the interview is Node-flavored, mention:
process.nextTick > promise microtasks > timers.setImmediate runs after I/O callbacks; setTimeout(fn, 0) ordering versus setImmediate is only deterministic inside an I/O callback.setTimeout(fn, 0) still delay?" — the callback must wait for the current macrotask plus all microtasks; browsers also clamp nested timers (~4 ms after 5 levels).setTimeout/requestIdleCallback, move it to a Web Worker, or use scheduler.yield() where supported.MutationObserver or React state updates?" — batched reactions run before paint, so reading layout between microtasks can force sync reflow.The event loop is the theory behind several implementation questions in this catalog: custom setTimeout, custom setInterval, debounce, and throttle all exist because of how task scheduling works.
What a closure really is, why loops with var trip people up, and how closures power memoization, once(), and module patterns.
The four binding rules, arrow-function behavior, and the lost-this bugs that call, apply, and bind exist to fix.
The prototype chain, __proto__ vs prototype, and what class syntax actually does under the hood.