What is the Event Loop?
JavaScript is single-threaded — only one thing runs at a time. The Event Loop is the mechanism that allows non-blocking async operations by coordinating the Call Stack, Web APIs, and task queues.
Call Stack
→
Web APIs
→
Callback Queue
→
Event Loop
→
Call Stack
Event Loop Cycle
1
Call Stack
Runs synchronous code
2
Web APIs
Handles async work offloaded from stack
3
Task Queue
Stores completed callbacks
4
Event Loop
Checks if stack is empty
5
Call Stack
Callback pushed and executes
When async work (timers, network calls) completes in Web APIs, their callbacks are pushed into the Callback Queue. The Event Loop moves them onto the Call Stack only when the stack is empty.
Microtasks vs Macrotasks
Not all async callbacks are equal. The event loop processes two distinct queues with different priorities.
| Microtasks | Macrotasks |
|---|---|
| Promise.then / catch / finally | setTimeout / setInterval |
| queueMicrotask() | MessageChannel |
| MutationObserver | requestAnimationFrame |
| Runs before next macrotask | Runs after all microtasks clear |
| Drains the entire queue each tick | One task per event loop tick |
Execution Priority (highest first)
Synchronous code100
Microtask (Promise.then)75
requestAnimationFrame50
Macrotask (setTimeout)25
✓Key rule
The entire microtask queue runs to completion after every task — including after each macrotask. Microtasks always take priority.
Execution Order
Understanding the exact order helps you reason about async code. The steps below describe one full tick of the event loop.
1
Synchronous code runs first
All synchronous statements execute top-to-bottom on the Call Stack. Nothing async runs yet.
2
Async callbacks are offloaded
setTimeout, fetch, and event listeners are handed to Web APIs. The call stack is freed immediately.
3
Microtask queue drains
When the stack empties, every pending microtask (Promise callbacks) runs to completion before any macrotask.
4
One macrotask executes
The event loop picks the oldest macrotask from the queue (e.g. a setTimeout callback) and runs it.
5
Repeat
After each macrotask, the microtask queue drains again. This cycle continues until both queues are empty.
Execution order example
console.log("1 - start");
setTimeout(() => console.log("2 - setTimeout"), 0);
Promise.resolve().then(() => console.log("3 - promise"));
console.log("4 - end");
// Output:
// 1 - start
// 4 - end
// 3 - promise ← microtask runs first
// 2 - setTimeout ← macrotask runs lastCommon Pitfalls
!Blocking the call stack
Heavy synchronous work (large loops, synchronous XHR) blocks the entire thread. No UI updates, no event handling, no async callbacks can run while the stack is busy.
!Infinite microtask loop
Recursively scheduling microtasks (e.g. calling
queueMicrotask inside a microtask) starves the macrotask queue and freezes the page — the loop never advances to the next tick.isetTimeout(fn, 0) is not instant
A delay of 0 ms is clamped by browsers (typically to 1–4 ms) and still runs after all pending microtasks. It is not equivalent to synchronous execution.