ai js event loop 2026-09-04
Explain JavaScript event loop
Explain JavaScript event loop
(ai answer)
The JavaScript event loop is the runtime mechanism that lets single-threaded JavaScript handle asynchronous work without blocking the main thread. It coordinates the call stack, host APIs (browser Web APIs or Node APIs), the macrotask queue, and the microtask queue. When the call stack is empty, the loop decides what runs next according to a strict priority order.
JavaScript itself has one call stack and can execute only one piece of code at a time. Long-running work (timers, network I/O, user events) is handed off to the host environment. When that work finishes, a callback is placed in a queue. The event loop is what moves those callbacks onto the stack at the right time.
The main pieces
- Call stack — LIFO stack of currently executing functions. A function runs to completion (run-to-completion); it cannot be interrupted by another JS function on the same thread.
- Web APIs / Node APIs — The environment that actually waits (timers,
fetch, DOM events, file I/O). These are not part of the JS engine. - Macrotask queue (task queue) —
setTimeout/setInterval, I/O completion, UI events (click,keydown), script execution itself. - Microtask queue — Promise
.then/.catch/.finally,queueMicrotask(),MutationObserver, and the continuation afterawait. - Event loop — The coordinator: “If the stack is empty, drain every microtask, then take one macrotask, then repeat.” In browsers, rendering can happen between iterations.
A typical diagram looks like this:
Another common view that separates the two queues:
The loop’s algorithm
- Run the current synchronous code until the call stack is empty.
- Drain the entire microtask queue (including any microtasks that those microtasks themselves enqueue).
- (Browser) Optionally update rendering / paint.
- Dequeue and run one macrotask.
- Go back to step 2.
That asymmetry is the most important rule: all pending microtasks run before the next macrotask and before the browser paints.
Microtasks vs macrotasks
| Microtask | Macrotask | |
|---|---|---|
| Examples | Promise.then, queueMicrotask, await continuation, MutationObserver |
setTimeout, setInterval, I/O, UI events, setImmediate (Node) |
| When it runs | After every task / when the stack empties | One per event-loop turn |
| How many run | The whole queue, even newly added ones | Exactly one, then microtasks drain again |
| Rendering | Browser will not paint until the queue is empty | Paint can occur between macrotasks |
A classic demonstration:
console.log('script start'); setTimeout(() => console.log('setTimeout'), 0); Promise.resolve() .then(() => console.log('promise 1')) .then(() => console.log('promise 2')); console.log('script end');
Output order:
script startscript end(synchronous)promise 1promise 2(microtasks drain completely)setTimeout(the next macrotask)
setTimeout(..., 0) is not “run immediately”; it is a macrotask that waits until after all current microtasks.
Why this design exists
The model keeps the UI responsive: while a network request or timer is pending, the thread can still process clicks, animations, and other work. I/O is non-blocking by design. The trade-off is that a flood of microtasks (for example an infinite chain of Promise.then) can starve macrotasks and rendering — the page appears frozen even though the stack keeps emptying.
Node.js differences
Node’s event loop is organized into phases (timers → pending callbacks → poll → check → close, plus idle/prepare). process.nextTick has even higher priority than Promise microtasks. The same microtask-vs-macrotask priority still applies inside each phase, but the extra phases matter for setImmediate vs setTimeout and for I/O-heavy servers.
Once you internalize “empty the stack → drain every microtask → one macrotask,” the execution order of almost any mix of promises, async/await, timers, and events becomes predictable.