Two queues, not one
Earlier we saw that setTimeout(fn, 0) is deferred until the current code
finishes. But there's a finer detail: the event loop doesn't have one queue,
but (at least) two, and they don't have the same priority.
- Macrotask queue: this is where
setTimeout,setIntervalcallbacks, browser events, etc. go. - Microtask queue: this is where promise reactions (
.then,.catch,.finally) and the body that follows anawaitgo.
The golden rule
Every time the main thread finishes a task, it fully drains the microtask queue before taking the next macrotask. In other words: microtasks are the "express line" that goes first; macrotasks wait in the back of the line.
Think of it like a supermarket checkout: the macrotask is the next customer with their full cart, but before charging them, the cashier helps everyone who just comes "to ask something quick" (the microtasks). Even though the customer with the cart arrived "first" (delay 0), the quick questions are resolved first.
console.log("1: synchronous");
setTimeout(() => console.log("2: macrotask (setTimeout 0)"), 0);
Promise.resolve().then(() => console.log("3: microtask (.then)"));
console.log("4: synchronous");
// Output order: 1, 4, 3, 2
First all the synchronous code runs (1, 4). Then, before touching the
setTimeout macrotask, the microtask queue is drained (3). Only then does
the macrotask's turn arrive (2).
await is also a microtask
The code that follows an await resumes as a microtask. That's why, in
practice, promises always "beat" a setTimeout(0) scheduled at the same time.
Careful: if you chain a ton of microtasks that generate more microtasks, you can starve the macrotasks (including the page's rendering). Microtasks are fast, but they aren't free.
Examples
The classic interview order
console.log("A");
setTimeout(() => console.log("B (setTimeout)"), 0);
Promise.resolve().then(() => console.log("C (then)"));
console.log("D");
// Output: A, D, C, B
Chained microtasks go before the macrotask
setTimeout(() => console.log("macro"), 0);
Promise.resolve()
.then(() => console.log("micro 1"))
.then(() => console.log("micro 2"));
console.log("synchronous");
// Output: synchronous, micro 1, micro 2, macro