JavaScript runs on a single thread, but clearly does things "later" all the time — timers fire, network responses arrive, Promises resolve. The event loop is the mechanism that decides the order all of this deferred work actually runs in, using two separate queues.
.then()/.catch()/.finally() callbacks, queueMicrotask(), and (in Node) process.nextTick (which runs even before other microtasks)setTimeout, setInterval, I/O callbacks, UI events like clicksThe rule that explains almost every async ordering question: after the current synchronous code finishes, the engine drains the entire microtask queue — completely, including any new microtasks queued by earlier ones — before running even a single macrotask.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Output: 1, 4, 3, 2
Order of events: "1" and "4" run synchronously first. Then, before the setTimeout callback (a macrotask) is allowed to run, the engine fully drains the microtask queue — just the one .then() here — logging "3". Only after the microtask queue is completely empty does the next macrotask ("2") get its turn. This holds even with a 0ms delay, because the delay only controls when the callback is eligible to run, not that it preempts pending microtasks.
Because the engine won't move to the next macrotask until the microtask queue is fully empty, a microtask that keeps scheduling more microtasks can delay timers and rendering indefinitely:
function loop() {
Promise.resolve().then(loop); // schedules another microtask, forever
}
loop();
// setTimeout callbacks, clicks, and rendering never get a turn — the microtask queue never empties
This is a real, if uncommon, class of bug — an unbounded chain of chained Promise callbacks can make an app feel completely frozen even though technically "nothing is blocking" in the traditional synchronous-loop sense.
async/await fits into the queuesEverything after an await in an async function resumes as a microtask, once the awaited Promise settles — which is why the ordering rules above apply identically to async/await code, even though it doesn't visibly mention .then() anywhere:
console.log("start");
async function example() {
console.log("A");
await null; // yields — everything after this line is scheduled as a microtask
console.log("B");
}
example();
console.log("end");
// Output: start, A, end, B
await null still yields control back to the caller and resumes "B" as a microtask, exactly as if it were Promise.resolve(null).then(() => console.log("B")) — async/await is genuinely just Promise chaining with different syntax, all the way down to scheduling.