Frontend Labs
⋯

Part II — Intermediate: async JavaScript and the event loop

12. The call stack, heap, and the event loop model

JavaScript engines run on one call stack per realm (§37 explains what "realm" means precisely). Long-running synchronous code blocks that single stack completely — nothing else, including UI rendering, can happen until it unwinds. Understanding why async APIs exist, and how they resume, is the single highest-leverage piece of JS knowledge for building responsive apps.

   Call Stack                 Web APIs / Node APIs         Callback Queues
  ┌───────────┐             ┌──────────────────────┐     ┌─────────────────┐
  │  main()   │──setTimeout─▶│ timer (browser/libuv) │     │  Macrotask queue │
  │  fetch()  │──fetch──────▶│ network stack         │────▶│  (setTimeout,    │
  └───────────┘             └──────────────────────┘     │   I/O, UI events)│
        ▲                                                  └─────────────────┘
        │                                                           │
        │              Event Loop: "is the stack empty?"            │
        │              if yes: drain ALL microtasks first,          │
        └──────────────pop one task, push it onto the stack─────────┘
                              ┌─────────────────┐
                              │ Microtask queue  │  (Promise .then/.catch,
                              │                  │   queueMicrotask, async/await
                              └─────────────────┘   resumption)

The critical ordering rule, tested in every JS interview and hit in every real async bug: after each single task, the engine fully drains the microtask queue before running the next task (or the next paint).

console.log("1: sync");

setTimeout(() => console.log("2: macrotask (setTimeout)"), 0);

Promise.resolve().then(() => console.log("3: microtask (promise)"));

queueMicrotask(() => console.log("4: microtask (queueMicrotask)"));

console.log("5: sync");

// Output order: 1, 5, 3, 4, 2
// Both synchronous logs run first (the stack never yields mid-script).
// Then ALL queued microtasks drain (3 then 4, in queue order) —
// THEN, only once the microtask queue is empty, the engine picks up
// the next macrotask (2).

This is why a microtask that queues another microtask can, in principle, starve the event loop from ever reaching a macrotask (a real, documented performance bug class — an infinite chain of Promise.resolve().then(...) will hang a page just as thoroughly as a synchronous while(true)).

Generated from real-life-JS-guide.md · 8 Oct 2026 16:36