TL;DR
- JavaScript is single-threaded, but the browser and Node runtimes offload work and re-enter JavaScript through event-loop scheduling.
- In the browser, synchronous work runs first, then microtasks drain fully, then the runtime can render, then the next task runs.
- In Node.js, libuv phases matter, and
process.nextTickcan outrank promise microtasks. - “Async” does not guarantee responsiveness; long synchronous work and runaway microtasks can still freeze the app.
Context
The event loop is one of the most common senior-level interview topics because it explains real bugs: weird ordering, blocked rendering, Node process stalls, and “why did the timer fire later than I expected?” moments. I use it as the runtime mental model behind every promise, timer, fetch, and render cycle.
The Approach
In the browser, the mental model I use is:
- run the current synchronous work on the call stack
- drain the microtask queue fully
- allow render work if needed
- pick the next task
console.log("A")
setTimeout(() => console.log("B"), 0)
Promise.resolve().then(() => console.log("C"))
queueMicrotask(() => console.log("D"))
console.log("E")
// A, E, C, D, B
Microtasks matter because promises, queueMicrotask, and some observer callbacks use them. That makes them feel “very soon,” but it also creates starvation risk.
function spin() {
Promise.resolve().then(spin)
}
spin()
This can freeze the UI because microtasks drain before the browser gets a chance to paint or pick up the next task.
When I explain a button click lifecycle, I usually walk it this way:
- the browser receives the DOM event
- it enqueues the event handler as a task
- the handler runs synchronously on the call stack
- any resolved promises queue microtasks
- microtasks drain
- the browser can render
- the next task begins
For Node.js, I add one more layer: libuv phases. The practical takeaway is that Node is not “the same as the browser but without the DOM.” Timers, poll, check, and process.nextTick create ordering differences that matter in debugging and interview questions.
Trade-offs
- The event loop makes JavaScript’s async model approachable, but it also hides scheduling detail until something goes wrong.
- Microtasks are useful for promise consistency, but they can create starvation if chained carelessly.
- Browser and Node mental models overlap, but assuming they are identical usually creates debugging mistakes.