The Event Loop & Task Queue Mechanics
JavaScript is single-threaded, meaning it can only execute one line of code at a time on its main thread. Despite this limitation, it handles non-blocking asynchronous operations—such as network requests, file I/O, and timers—efficiently through the Event Loop.
Architecture of Asynchronous Execution
To handle asynchronous tasks without freezing the user interface, the JavaScript engine works alongside browser Web APIs (or Node.js C++ bindings) and two distinct execution queues.
JavaScript Asynchronous Architecture
┌─────────────────────────────────────────────────────────────┐
│ Call Stack (Synchronous Execution LIFO) │
└──────────────────────────────┬──────────────────────────────┘
│ Delegation
▼
┌─────────────────────────────────────────────────────────────┐
│ Web APIs (Timers, Fetch HTTP, DOM Events, File Systems) │
└──────────────────────────────┬──────────────────────────────┘
│ Callbacks Ready
▼
┌──────────────────────────────┬──────────────────────────────┐
│ Microtask Queue (High-Priority)│ Macrotask Queue (Task Queue)
│ ├── Promises (.then/catch) │ ├── setTimeout / setInterval │
│ ├── queueMicrotask() │ ├── setImmediate (Node.js) │
│ └── MutationObserver │ └── requestAnimationFrame │
└──────────────┬───────────────┴──────────────┬───────────────┘
│ │
└───────────────┬──────────────┘
│ Event Loop Tick
▼
┌─────────────────────────────────────────────────────────────┐
│ Call Stack (Executed when Call Stack is empty) │
└─────────────────────────────────────────────────────────────┘
Microtasks vs. Macrotasks (Task Priorities)
The Event Loop continuously monitors the Call Stack. When the stack is empty, it processes pending callbacks according to a strict priority hierarchy:
| Queue Type | Operations | Execution Rule / Priority |
|---|---|---|
| Microtask Queue | Promise callbacks, queueMicrotask(), process.nextTick (Node) | Highest Priority: Drained completely until empty before any macrotask runs. |
| Macrotask Queue | setTimeout, setInterval, setImmediate, I/O, UI Rendering | Normal Priority: Processes one single task per Event Loop tick, then checks microtasks again. |
The Event Loop Execution Algorithm
During every cycle ("tick") of the Event Loop, the runtime follows these exact steps:
- Execute Synchronous Code: Process all frames in the Call Stack until it is completely empty.
- Drain Microtask Queue: Process every single microtask currently in the queue. If a microtask schedules another microtask, it is executed in the same cycle.
- Render UI (Browser Only): Perform DOM re-paints and run
requestAnimationFramecallbacks if needed. - Execute One Macrotask: Dequeue and run the oldest single task from the Macrotask Queue.
- Repeat: Loop back to step 1.
Microtask Starvation
Because the Event Loop must drain the entire Microtask Queue before yielding execution to macrotasks or UI rendering, continuously enqueuing microtasks will block the main thread indefinitely:
// WARNING: This recursively starves the Macrotask Queue and freezes the UI!
function infiniteMicrotask() {
Promise.resolve().then(() => {
infiniteMicrotask(); // Enqueues another microtask infinitely
});
}
// infiniteMicrotask(); // Un-commenting will crash/freeze the thread!
Interactive Playground: Tracing Execution Order
Predict the log sequence of synchronous statements, setTimeout, and Promise.then callbacks:
Best Practices
- Use Promises/Microtasks for Instant State Updates: When you need async operations to resolve immediately before DOM repainting or downstream state changes occurs.
- Offload Heavy Loops with
setTimeout: Yield execution back to the browser frame engine by breaking intensive computations into macrotask chunks (setTimeout(fn, 0)). - Avoid Infinite Microtask Chaining: Never recursively queue microtasks (
queueMicrotaskor.then) without exit conditions; doing so blocks DOM rendering and input events.
Knowledge Check
Exercise Requirements:
Determine the exact console output order for the following code snippet:
Explanation:
- Synchronous execution logs
AandF. - Microtask Queue processes
CandE. - Inside microtask
C, a new macrotask (D) is scheduled behindB. - Macrotask Queue runs
Bfirst (oldest macrotask), then runsD.
Now that you have mastered Event Loop ordering and task queues, proceed to Promises and Async/Await!