One waiter, many tables
Picture a restaurant with exactly one waiter. That waiter is JavaScript.
The waiter can only do one thing at a time: take an order, carry a plate, or refill water. But the restaurant still serves forty tables, because the waiter never stands in the kitchen waiting for the food. They hand the order to the kitchen, go serve someone else, and come back when the bell rings.
The kitchen is the browser (or Node.js). The bell is the event loop. That's the whole idea. The rest is details, and the details are where the interview questions live.
The four characters
- Call stack: what the waiter is doing right now. Functions get pushed on when called and popped off when they return.
- Web APIs / libuv: the kitchen. Timers,
fetch, file reads and DOM events are handled outside your thread. When they're done, they drop a callback into a queue. - Microtask queue: the VIP line. Promise callbacks (
.then,awaitcontinuations) andqueueMicrotaskgo here. - Task queue (often called the macrotask queue): everyone else.
setTimeout,setInterval, UI events, I/O callbacks.
The one rule that explains everything
When the call stack is empty, the event loop does this, over and over:
- Run all microtasks, until the microtask queue is empty (including new ones added along the way).
- Let the browser render, if it wants to.
- Take one task from the task queue and run it.
- Go back to step 1.
So promises are VIPs: after every single task, the entire VIP line gets served before the next regular customer.
The classic quiz
What does this print?
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
If you said 1 2 3 4, welcome, you're among friends. The real answer is 1 4 3 2.
1and4are plain synchronous code, so they run first, top to bottom.- The promise callback (
3) is a microtask, so it runs as soon as the stack empties. - The
setTimeoutcallback (2) is a task. Even with a delay of0, it waits behind every microtask.setTimeout(fn, 0)means "not sooner than 0 ms", not "right now".
How to freeze a web page in one line
Because there's one waiter, if you make the waiter do a 5-second job, every table waits 5 seconds. Clicks, scrolling, animations: frozen.
button.addEventListener("click", () => {
const end = Date.now() + 5000;
while (Date.now() < end) {} // congratulations, the page is now a screenshot
});
Ways to be nicer to your waiter:
- Split big work into chunks and yield between them (for example with
setTimeoutorscheduler.yield()where available), so the browser can render and handle input. - Move heavy CPU work to a Web Worker (or
worker_threadsin Node). That's hiring a second waiter who works in another room. - Don't create infinite microtasks. A promise chain that keeps scheduling more microtasks can starve rendering, because the VIP line never empties.
A note for Node.js folks
Node has the same idea, powered by libuv, with a few extra phases (timers, I/O polling, setImmediate checks). Two Node-specific things to remember:
process.nextTickcallbacks run before promise microtasks. It's the VIP line for the VIP line.setImmediateruns in the "check" phase, right after I/O polling. Inside an I/O callback,setImmediatealways fires before asetTimeout(fn, 0).
Once this clicks, a lot of "why is this undefined" and "why did this log late" bugs stop being mysterious. They're just a waiter following very strict rules.
