Reading — step 1 of 7
Learn
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:
- Call stack — the function-call stack of the currently running synchronous code
- Microtask queue — Promise callbacks, queueMicrotask(), MutationObserver
- 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
Why?
- Synchronous:
1, then schedule timeout, schedule promise, schedule microtask, then5 - Stack empty → drain microtasks:
3,4 - 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:
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):
- Timers — setTimeout/setInterval callbacks
- Pending callbacks — system errors, etc.
- Idle, prepare (internal)
- Poll — I/O callbacks (the bulk of work happens here)
- Check — setImmediate callbacks
- 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).
Common patterns and pitfalls
Microtask starvation:
Yielding to the loop:
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
MessageChannelorqueueMicrotaskfor 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 priority —
awaitcontinuations 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…