JavaScript event loop.
JavaScript runs on one thread. It can\'t block — if it did, the page would freeze. So everything async gets queued: timers, promises, fetches, clicks. The event loop is a small piece of runtime code that empties the call stack, then drains microtasks, then takes one macrotask, then repeats. Watch four lines of code produce A D C B instead of A B C D — and see exactly why.
log("A") · setTimeout(B, 0) · Promise.then(C) · log("D") expected output (naive): A B C D · actual output: A D C B- Call stack
- The same stack we visited in the call-stack visualization — function calls push, returns pop. JS runs strictly single-threaded on this stack.
- Microtask queue
- High-priority callbacks: Promise.then, queueMicrotask, MutationObserver. Drained completely after every macrotask, before the next macrotask runs.
- Macrotask queue
- Lower-priority callbacks: setTimeout, setInterval, I/O callbacks, UI events. One task processed per event-loop tick.
Why the order matters
A function that does heavy work between scheduling a Promise.then and scheduling a setTimeout will delay both — but it will still delay the setTimeout more. The microtask drains right after the function returns; the macrotask waits for the next event loop tick. In practice this means: anything you write as await or .then runs effectively "right after" the current call returns. Anything you write as setTimeout(0) waits at least one full loop iteration.
A second consequence: microtasks can starve macrotasks. A microtask that enqueues another microtask, recursively, will keep the loop in the drain step forever. Macrotasks (including UI rendering) never get a turn. Most runtimes don\'t have a safeguard for this; the page will freeze.
await is sugar for promise.then
const x = await foo(); is sugar for foo().then(x => /* rest of function */). The "rest of the function" is a microtask. This is why async functions feel synchronous but don\'t block — and why await Promise.resolve() is a clean way to yield to the microtask queue without yielding all the way to a macrotask.
Browser vs Node differences
- Rendering. Browsers run a render step in the loop, typically between macrotasks. Long-running JS blocks paint. Web Workers exist precisely to move heavy work off the main thread.
- setImmediate. Node has setImmediate, a macrotask that always runs in the next iteration of the loop. Browsers don\'t have it.
- process.nextTick. Node has process.nextTick which runs before microtasks. Confusingly, it\'s higher priority than promises.
- requestAnimationFrame. Browser-only, runs callbacks right before the next paint. The right place to do DOM mutations tied to animation.
What this simplifies
- No tasks vs microtask queues per task source. The spec defines multiple task queues (timer, network, user, etc.) and the runtime picks one per tick. We collapsed them into "macrotask queue."
- No render step. Real browsers run layout + paint between macrotasks. We omitted it — JS-only programs (Node) don\'t have one.
- No async/await stack traces. The runtime maintains a "logical" stack across awaits for debugging. The actual machine stack does unwind at each await.
- No background threads. The JS engine has worker threads (GC, compiler, parser). The event loop is the main thread\'s loop; it\'s where your code runs.
Languages Codex →
Async runtimes, promises in depth, scheduling in Node vs browser vs Deno, why goroutines are a different model entirely, what await actually compiles to.
Open the Codex →