The Event Loop
intermediateHow the call stack, microtask queue, and macrotask queue decide execution order — with the output-prediction questions interviewers love.
What closures are, how lexical scope works, the classic var-in-a-loop question, and how closures power memoize, once, and module patterns in interviews.
A closure is a function bundled with the lexical scope it was created in. When a function references a variable from an enclosing scope, JavaScript keeps that variable alive for as long as the function exists — even after the outer function has returned.
function makeCounter() {
let count = 0; // lives on after makeCounter returns
return function () {
count += 1;
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
const other = makeCounter();
other(); // 1 — each call to makeCounter creates a fresh scopeTwo things interviewers listen for in your explanation:
count, not a snapshot from creation time.counter and other are independent because they close over different count variables.This is the most-asked closure question in existence:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3, 3, 3Why? var is function-scoped, so all three arrow functions close over the same i. By the time the timers fire (after the loop, per the event loop), i is 3.
Interviewers then ask you to fix it — know all three fixes and their trade-offs:
// 1. let — block scoping creates a fresh binding per iteration
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}
// 2. IIFE — capture the value in a new function scope (pre-ES6 style)
for (var i = 0; i < 3; i++) {
((j) => setTimeout(() => console.log(j), 0))(i);
}
// 3. setTimeout's extra arguments — least known, most elegant
for (var i = 0; i < 3; i++) {
setTimeout(console.log, 0, i);
}Most implementation questions are secretly closure questions. If you can articulate where the state lives, the implementations follow:
once() — closes over a called flag and a cached result.memoize() — closes over a cache Map.debounce / throttle — close over a timer id and timestamps.curry — closes over the arguments collected so far.A compact example that interviewers love because it combines two of these:
function once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}The returned function is the closure: called and result are invisible to callers, impossible to tamper with, and garbage-collected only when the function itself is.
Before class fields and #private names, closures were JavaScript's only privacy mechanism — and the pattern still appears in interviews:
function createBankAccount(initial) {
let balance = initial; // truly private
return {
deposit(amount) { balance += amount; return balance; },
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() { return balance; },
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.balance; // undefined — no way to reach it directlyClosures keep scopes alive, which means they can leak memory when a long-lived function captures a large object it no longer needs:
function attach(bigData) {
// handler closes over bigData forever, even if it only needs the length
document.addEventListener("click", () => console.log(bigData.length));
}The fix is to capture only what you need (const len = bigData.length) or to remove listeners when done. Strong answers mention that modern engines can optimize what a closure retains, but you shouldn't rely on it — captured objects referenced anywhere in the inner function are kept.
this, lexically — see the this keyword).deposit and withdraw above); that's the basis of the module pattern.How the call stack, microtask queue, and macrotask queue decide execution order — with the output-prediction questions interviewers love.
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.