10 · 15 steps
Visualize / 10

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.


step 1 / 15 · output:
script 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
MICROTASK
MACROTASK
CONSOLE
SOURCE 1 console.log("A"); 2 setTimeout(() => console.log("B"), 0); 3 Promise.resolve().then(() => console.log("C")); 4 console.log("D"); 5 // — synchronous code done — 6 // microtasks drain first 7 // then one macrotask per loopCONSOLE OUTPUT (live)— empty —EVENT LOOP RULES1. run sync to stack-empty2. drain ALL microtasks3. take ONE macrotask4. repeat from step 2CALL STACK0— empty —MICROTASK QUEUE0— empty —MACROTASK QUEUE0— empty —WHAT JUST HAPPENED— event loop idle —
Initial — script about to run
The runtime is about to start executing your top-level script. Call stack is empty. Both queues are empty. The "event loop" itself is a small piece of runtime code that does exactly two things repeatedly: empty the call stack, then drain microtasks, then take one macrotask.
QUEUE TAXONOMYMICROTASKSPromise.then / catch / finallyawait (sugar for .then)queueMicrotask()MutationObserverdrained to empty each tickMACROTASKSsetTimeout / setIntervalsetImmediate (Node)I/O · fetch fulfilmentUI events · messageONE per tick
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.
Go deeper

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 →
Found this useful?