Skip to content
Event Loop and Microtasks
step 1/7

Reading — step 1 of 7

Learn

~3 min readThe Runtime

JavaScript is single-threaded. The event loop is what makes async code work — it processes tasks from queues one at a time. Understanding the loop's execution order is essential for any non-trivial async code.

The execution model

At any moment the runtime has:

  1. Call stack — the function-call stack of the currently running synchronous code
  2. Microtask queue — Promise callbacks, queueMicrotask(), MutationObserver
  3. Macrotask queue (a.k.a. task queue) — setTimeout, setInterval, setImmediate (Node), I/O callbacks, UI events

The loop's rule:

Loop forever:
  1. Run synchronous code until the call stack is empty.
  2. Drain the ENTIRE microtask queue (running them adds to the stack; drain again).
  3. Render (browsers — paint a frame if needed).
  4. Pull ONE macrotask from the queue, run it.
  5. Go to step 2.

Key takeaway: microtasks run before the next macrotask. A long microtask chain can starve macrotasks (and rendering).

Order surprises

javascript

Why?

  1. Synchronous: 1, then schedule timeout, schedule promise, schedule microtask, then 5
  2. Stack empty → drain microtasks: 3, 4
  3. Pull macrotask: 2

setTimeout(fn, 0) doesn't mean "now." It means "after the current microtask drain."

await and the queue

An await schedules the rest of the function as a microtask:

javascript
  • 1, A (sync part of run), 2 (back in main)
  • Microtask drain: B (continuation of run after await)

Node-specific phases

Node's event loop has phases (per libuv):

  1. Timers — setTimeout/setInterval callbacks
  2. Pending callbacks — system errors, etc.
  3. Idle, prepare (internal)
  4. Poll — I/O callbacks (the bulk of work happens here)
  5. Check — setImmediate callbacks
  6. Close — close events

Between EACH phase, the microtask queue is drained AND process.nextTick callbacks run (nextTick is even higher priority than microtasks in Node).

javascript

Common patterns and pitfalls

Microtask starvation:

javascript

Yielding to the loop:

javascript

Lets the runtime do other work — render, handle clicks, run timers. Use in long-running async loops.

Why setTimeout(..., 0) minimum is ~4ms in browsers:

  • HTML spec floor for nested timeouts (after 5 levels of nesting)
  • Use MessageChannel or queueMicrotask for genuinely-zero-delay scheduling

Common mistakes

  • Assuming setTimeout(fn, 0) is immediate — it's a macrotask scheduled for the next loop turn.
  • Assuming Promise resolution is synchronous.then() callbacks always run as microtasks, even if the Promise is already resolved.
  • Long synchronous loops blocking the UI — chunk work with await + setTimeout, or use a Worker.
  • Forgetting microtask priorityawait continuations queue ahead of timers, even if the timer was scheduled first.
  • Recursive Promise.then chains — starve macrotasks. Insert occasional setTimeout(0) to let the loop breathe.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…