Knowing the event loop explains UI freezes, ordering bugs and why await doesn't block.
JavaScript runs on one thread per agent (window, worker or Node process). The engine executes code from a call stack; when the stack empties, the host's event loop decides what runs next.
There are two kinds of queued work:
- Tasks (macrotasks): running a script, timer callbacks, I/O callbacks, user events,
MessageChannelmessages. The loop takes one task at a time. - Microtasks: promise reactions (
then/catch/finallycallbacks, and code after anawait),queueMicrotask(fn)andMutationObservercallbacks.
The rule that predicts ordering: after each task — whenever the stack empties — the entire microtask queue is drained, including microtasks queued by other microtasks, before the next task starts. In browsers, rendering (style, layout, paint, and requestAnimationFrame callbacks) happens between tasks, roughly once per frame.
Consequences:
Promise.resolve().then(f)always runs beforesetTimeout(g, 0).- An
asyncfunction runs synchronously until its firstawait; the rest continues as a microtask. - A long synchronous loop blocks everything: no clicks, no rendering, no timers. So does an endless chain of microtasks — they starve the loop even though each is "async".
To keep the page responsive, split long work into chunks and yield to the loop between them (await new Promise(r => setTimeout(r)), or scheduler.yield() where supported), or move it to a Web Worker.
Node.js has the same model with extra phases (timers, poll for I/O, check for setImmediate) and process.nextTick, whose queue runs even before promise microtasks.
Example
console.log("1: script start");
setTimeout(() => console.log("8: timeout (next task)"), 0);
Promise.resolve()
.then(() => console.log("4: then #1 (microtask)"))
.then(() => console.log("7: then #2 (queued when #1 ran)"));
queueMicrotask(() => console.log("5: queueMicrotask"));
(async () => {
console.log("2: async fn runs synchronously until await");
await null;
console.log("6: code after await (microtask)");
})();
console.log("3: script end");
// Logs appear in numeric order: 1 2 3 4 5 6 7 8Remember
- Microtasks (promises, await, queueMicrotask) run before the next task
- The microtask queue is fully drained after each task — even newly added ones
- Long synchronous work blocks input and rendering; chunk it or use a worker
Watch out for
- Assuming
setTimeout(fn, 0)runs before already-resolved promise callbacks.