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?

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
- Code runs synchronously, pushing function frames onto the Call Stack.
- Asynchronous operations like
setTimeoutorfetchregister with Web APIs. - Upon completion, their callbacks are placed in the Callback Queue.
- The Event Loop monitors the call stack; once the stack is clear, it dequeues tasks onto the stack.
Example: setTimeout Execution Order
JAVASCRIPT1console.log("Start"); 2 3setTimeout(() => { 4 console.log("Callback"); 5}, 0); 6 7console.log("End");
Output:
JAVASCRIPT1Start 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:
Promisecallbacks (.then,.catch,.finally),queueMicrotask,MutationObserver.
All microtasks are drained completely before the runtime picks the next macrotask:
JAVASCRIPT1console.log("1"); 2 3Promise.resolve().then(() => { 4 console.log("2"); 5}); 6 7setTimeout(() => { 8 console.log("3"); 9}, 0); 10 11console.log("4");
Output:
JAVASCRIPT11 24 32 43
Common Pitfalls
- 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.
- Microtask Queue Starvation: Infinite recursive Promise resolution will block the macrotask queue and UI rendering.
- Timer Jitter:
setTimeoutdefines 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.