The Event Loop Demystified
Breaking down the event loop — the mechanism at the heart of JavaScript's concurrency model — to understand what actually happens when your code runs.
JavaScript is single-threaded. That statement confuses people because they see async/await, setTimeout, and fetch requests all happening “in parallel.” The key to understanding this is the event loop.
One Thread, Many Tasks
JavaScript has exactly one call stack. It runs one thing at a time. Period.
But the runtime environment — your browser or Node.js — has more than just the JS engine. It has:
- Web APIs / C++ bindings (timers, network, file I/O)
- A task queue (callbacks waiting to run)
- A microtask queue (promises,
queueMicrotask) - The event loop itself (the coordinator)
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("end");
Output:
start
end
microtask
timeout
This isn’t random. It’s deterministic, and understanding why this order happens is understanding the event loop.
The Loop
The algorithm, simplified:
- Execute everything on the call stack until it’s empty
- Drain the microtask queue completely
- Pick one task from the task queue, execute it
- Repeat
Microtasks always run before the next task. This is why promise callbacks fire before setTimeout callbacks, even with a 0ms delay.
Why This Matters
You Can Block the Loop
// This freezes your entire UI for ~5 seconds
const start = Date.now();
while (Date.now() - start < 5000) {
// spinning
}
There’s no “background thread” to save you. If your synchronous code takes too long, everything waits — rendering, user input, network callbacks, all of it.
Microtask Starvation
Because microtasks drain completely before the next task, you can accidentally starve the task queue:
function recurse() {
Promise.resolve().then(recurse);
}
recurse(); // This never yields to the task queue
This is a subtle but real footgun. Each .then adds a microtask, and the loop will keep draining microtasks forever.
The Mental Model
Think of it like a restaurant with one chef:
- Call stack = the dish currently being prepared
- Microtask queue = urgent fixes to the current dish (“add more salt”)
- Task queue = the next order ticket
- Event loop = the chef checking what to do next
The chef finishes the current dish, handles all the urgent fixes, then picks up the next order. Never two orders at once.
The event loop isn’t magic. It’s a simple, deterministic loop. But it’s the foundation everything else — async/await, React rendering, Node.js servers — is built on top of. Once you see it clearly, asynchronous JavaScript stops being confusing.