The JavaScript Event Loop: How Single-Threaded JS Handles Concurrency
JavaScript is strictly single-threaded, yet it serves thousands of concurrent requests without freezing. Learn the call stack, Web APIs, callback queue, and the microtask vs. macrotask priority system that make it possible.
5 sections · ~20 min · 6-question quiz (pass ≥ 70%)
1The Paradox: Single-Threaded Yet Concurrent
It sounds like a massive contradiction: JavaScript is strictly single-threaded (meaning it has only one call stack and can execute exactly one line of code at a time), yet it handles thousands of concurrent server requests or heavy user interfaces smoothly without freezing.
It pulls off this trick because JavaScript itself doesn't work alone. It runs inside a hosting environment (like a Web Browser or Node.js) that provides a larger ecosystem: the Event Loop.
Here is how JavaScript juggles multiple tasks without blocking the main thread.
2The Architecture: The Ecosystem Around the Thread
To understand the Event Loop, you have to look at the four main pieces working together inside the browser or runtime environment:
- The Call Stack: The single thread where your actual JavaScript code executes, one function at a time (Last In, First Out).
- Web APIs (or Node.js APIs): Background threads managed by the browser/runtime. This is where the real multi-threading happens behind the scenes for tasks like timers (
setTimeout), network requests (fetch), or file reads. - The Callback Queue (Task Queue): A holding pen where asynchronous tasks wait once the Web API finishes processing them.
- The Event Loop: A relentless orchestrator with just one job: look at the Call Stack, look at the Callback Queue, and move waiting tasks over if the stack is completely empty.
3The Step-by-Step Lifecycle
Let's look at what happens under the hood with a classic piece of asynchronous code:
console.log("Start");
setTimeout(() => {
console.log("Inside Timeout");
}, 2000);
console.log("End");
Here is exactly how the system handles this program step-by-step:
- Execute synchronous code — Call Stack.
console.log("Start")is pushed onto the Call Stack, prints"Start", and is immediately popped off. - Hand off async tasks — Web APIs. The
setTimeoutfunction is pushed to the Call Stack. Because it's an asynchronous API, the JavaScript engine doesn't wait. It hands the timer and the callback function over to the Web APIs background environment and popssetTimeoutoff the stack instantly. - Continue main thread — Call Stack.
console.log("End")is pushed onto the stack, prints"End", and is popped off. The Call Stack is now completely empty. - Queue the callback — Callback Queue. Meanwhile, in the background, the Web API timer ticks down for 2000ms. Once it finishes, the Web API moves the
() => { console.log("Inside Timeout") }function into the Callback Queue. - The Event Loop triggers — Event Loop. The Event Loop constantly checks: "Is the Call Stack empty?" Yes. "Is there anything in the Callback Queue?" Yes. It grabs the callback function from the queue and pushes it onto the Call Stack.
- Execute callback — Call Stack. The callback executes, prints
"Inside Timeout", and the stack is clear once again.
4The Fast Track: Microtasks vs. Macrotasks
To make things slightly more interesting, the Callback Queue is actually split into two different tiers with different priorities:
┌────────────────────────────────────────────────────────┐
│ THE EVENT LOOP │
│ │
│ ┌───────────────┐ ┌───────────────────────┐ │
│ │ CALL STACK │ │ MICROTASK QUEUE │ │
│ │ │ ◄─────── │ (Promises, queueMicro)│ │
│ └───────────────┘ └───────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ └──────────────────────────────┼──────────────┤
│ │ │
│ ┌───────────────────────┐ │
│ │ MACROTASK QUEUE │ │
│ │ (setTimeout, I/O) │ │
│ └───────────────────────┘ │
└────────────────────────────────────────────────────────┘
1. Macrotasks (Task Queue). These are standard asynchronous tasks like setTimeout, setInterval, user clicks, and network I/O. The event loop processes only one macrotask per loop cycle.
2. Microtasks. These are high-priority tasks, specifically Promise callbacks (.then, .catch, await) and MutationObserver.
The Golden Rule of the Event Loop: The Call Stack will never move on to the next macrotask until the Microtask Queue is completely empty. Even if microtasks keep adding more microtasks, the engine will clear them all out first before picking up the next timer or UI event.
5Test Your Intuition
What order do you think these print out in?
setTimeout(() => console.log("Timeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
console.log("Sync");
"Sync"prints first because it is immediate and synchronous."Promise"prints second because it lands in the high-priority Microtask Queue."Timeout"prints last because it lands in the standard Macrotask Queue, which has to wait for the microtask queue to clear out.
Once this ordering feels natural, you understand the event loop. Head to the quiz to lock it in.