Skip to main content

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 TypeOperationsExecution Rule / Priority
Microtask QueuePromise callbacks, queueMicrotask(), process.nextTick (Node)Highest Priority: Drained completely until empty before any macrotask runs.
Macrotask QueuesetTimeout, setInterval, setImmediate, I/O, UI RenderingNormal 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:

  1. Execute Synchronous Code: Process all frames in the Call Stack until it is completely empty.
  2. Drain Microtask Queue: Process every single microtask currently in the queue. If a microtask schedules another microtask, it is executed in the same cycle.
  3. Render UI (Browser Only): Perform DOM re-paints and run requestAnimationFrame callbacks if needed.
  4. Execute One Macrotask: Dequeue and run the oldest single task from the Macrotask Queue.
  5. 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:

Infinite Microtask Starvation Example
// 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:

Event Loop Playground
JavaScript Runtime
Console Output

Best Practices​

  1. Use Promises/Microtasks for Instant State Updates: When you need async operations to resolve immediately before DOM repainting or downstream state changes occurs.
  2. Offload Heavy Loops with setTimeout: Yield execution back to the browser frame engine by breaking intensive computations into macrotask chunks (setTimeout(fn, 0)).
  3. Avoid Infinite Microtask Chaining: Never recursively queue microtasks (queueMicrotask or .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:

Event Loop Exercise
JavaScript Runtime
Console Output

Explanation:​

  1. Synchronous execution logs A and F.
  2. Microtask Queue processes C and E.
  3. Inside microtask C, a new macrotask (D) is scheduled behind B.
  4. Macrotask Queue runs B first (oldest macrotask), then runs D.
Next Up

Now that you have mastered Event Loop ordering and task queues, proceed to Promises and Async/Await!