Back to JavaScript
JavaScript
medium
mid

How does the JavaScript event loop work?

JavaScript is single-threaded with one call stack. Async work (timers, fetch, events) is handed to Web APIs / the host. When it completes, a callback is queued. The event loop runs: when the call stack is empty, it drains ALL microtasks (Promises, queueMicrotask), then takes ONE macrotask (timer, I/O, event) and repeats — rendering can happen between macrotasks.

6 min read·~10 min to think through

JavaScript runs on a single thread with one call stack. The event loop is the mechanism that lets a single-threaded language do non-blocking async work.

The pieces

  • Call stack — where function frames are pushed/popped. Only one thing runs at a time.
  • Web APIs / host environment — the browser (or Node) provides setTimeout, fetch, DOM events, etc. These run outside the JS engine, often on other threads.
  • Macrotask queue (task queue) — callbacks from setTimeout, setInterval, I/O, UI events, MessageChannel.
  • Microtask queue — callbacks from resolved Promises (.then/await continuations), queueMicrotask, MutationObserver.
  • Event loop — the coordinator.

The algorithm

ts
while (true) {
  // 1. Run the current macrotask to completion (synchronous code runs here)
  // 2. Drain the ENTIRE microtask queue
  //    (microtasks added during this step also run, before moving on)
  // 3. Render if needed (style, layout, paint) — browser only, ~60fps
  // 4. Take ONE macrotask from the queue, go to 1
}

Two rules that explain almost every interview puzzle:

  1. The stack must be empty before the loop picks up any queued callback.
  2. All microtasks drain before the next macrotask — and before rendering.

Canonical example

js
console.log("1");
setTimeout(() => console.log("2"), 0);     // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4");
// Output: 1, 4, 3, 2

1 and 4 are synchronous. The stack empties. Microtasks drain → 3. Then one macrotask → 2.

Why it matters

  • A long synchronous task blocks everything — no events, no rendering, no timers. (This is why you offload heavy work to Web Workers or chunk it.)
  • A microtask that keeps queuing microtasks can starve the macrotask queue and rendering — an infinite .then loop freezes the page.
  • setTimeout(fn, 0) doesn't run "immediately" — it runs after the current task and all microtasks, and not before the minimum clamp (~4ms for nested timers).

Senior framing

The senior answer ties it to UX: the event loop is why JS feels responsive despite being single-threaded, and why one bad synchronous loop kills frame rate. Knowing that microtasks fully drain before rendering — and that this is how Promise-heavy code can still jank the UI — is the depth interviewers look for.

Follow-up questions

  • What's the difference between a microtask and a macrotask?
  • Why does setTimeout(fn, 0) not run immediately?
  • How can microtasks starve the event loop?
  • Where does rendering fit in the loop?

Common mistakes

  • Thinking setTimeout(fn, 0) runs before resolved Promises.
  • Believing JS is multi-threaded because async exists.
  • Forgetting the call stack must be empty before any callback runs.
  • Assuming rendering happens between every microtask.

Edge cases

  • Nested setTimeout calls are clamped to ~4ms minimum after the 5th level.
  • An unbounded microtask chain blocks rendering and macrotasks entirely.
  • Node's event loop has distinct phases (timers, poll, check) — not identical to the browser's.

Real-world examples

  • Why a heavy synchronous JSON parse freezes the UI; why Promise.then chains run before timers.

Related questions