Technical Publication2025-06-28

Event Loop: Understanding JavaScript Asynchronous Architecture

In the world of JavaScript, asynchronous behavior is everywhere: clicks, timers, and network API calls. Understanding how JavaScript handles concurrent operations on a single thread requires exploring the Event Loop.

This post explores how the Event Loop works, why it is critical for non-blocking performance, and how to avoid common concurrency pitfalls.


What is the Event Loop?

Event Loop Diagram

JavaScript is single-threaded, meaning it possesses a single call stack and executes one operation at a time. The Event Loop enables non-blocking I/O operations by delegating asynchronous tasks to browser Web APIs or Node.js libuv worker threads, then queuing their callbacks for execution when the call stack becomes empty.


The Call Stack, Web APIs, and the Task Queue

Here are the primary components of JavaScript asynchronous runtime:

  • Call Stack: where execution frames and functions get processed.
  • Web APIs / C++ Bindings: platform subsystems handling timers, network calls, and file I/O.
  • Callback Queue (Task Queue): where completed async callbacks wait for call stack availability.

Execution Cycle

  1. Code runs synchronously, pushing function frames onto the Call Stack.
  2. Asynchronous operations like setTimeout or fetch register with Web APIs.
  3. Upon completion, their callbacks are placed in the Callback Queue.
  4. The Event Loop monitors the call stack; once the stack is clear, it dequeues tasks onto the stack.

Example: setTimeout Execution Order

JAVASCRIPT
1console.log("Start"); 2 3setTimeout(() => { 4 console.log("Callback"); 5}, 0); 6 7console.log("End");

Output:

JAVASCRIPT
1Start 2End 3Callback

Even with a 0ms delay, the callback is queued until all synchronous stack frames finish executing.


Microtasks vs Macrotasks

JavaScript distinguishes between two task priorities:

  • Macrotasks: setTimeout, setInterval, setImmediate, I/O events.
  • Microtasks: Promise callbacks (.then, .catch, .finally), queueMicrotask, MutationObserver.

All microtasks are drained completely before the runtime picks the next macrotask:

JAVASCRIPT
1console.log("1"); 2 3Promise.resolve().then(() => { 4 console.log("2"); 5}); 6 7setTimeout(() => { 8 console.log("3"); 9}, 0); 10 11console.log("4");

Output:

JAVASCRIPT
11 24 32 43

Common Pitfalls

  1. Blocking the Call Stack: Long-running CPU loops freeze the entire event loop and user interface. Offload heavy computation to Web Workers or background worker threads.
  2. Microtask Queue Starvation: Infinite recursive Promise resolution will block the macrotask queue and UI rendering.
  3. Timer Jitter: setTimeout defines a minimum threshold, not an absolute guarantee of exact execution timing.

Conclusion

Mastering the Event Loop, microtasks, and macrotasks provides the foundation for building high-performance, responsive web applications and resilient backend services in Node.js.

Published by Jayant Verma • Full-Stack Software Engineer & AI Architect