The Event Loop
How One-Threaded JavaScript Never Freezes
LinkedIn Hook
JavaScript runs on ONE thread. One call stack. One instruction at a time.
And yet your app fetches data, responds to clicks, plays animations, and runs timers — all without freezing. How is that possible?
The answer is the Event Loop — the single most-asked concept in JavaScript interviews, and the one candidates explain worst.
Here is the trick the Event Loop pulls off: when your synchronous code hits a
setTimeout, afetch, or a click handler, it does NOT wait. It hands the work to the browser, keeps running, and trusts a "waiter" (the Event Loop) to hand the callback back to the call stack later — but only when the stack is empty.That single rule — "the stack must be empty" — is why
setTimeout(fn, 0)is not actually zero milliseconds, and why one badforloop can block a whole page.In this lesson you'll build a mental model for the Event Loop using a one-chef restaurant, walk through a step-by-step trace of the classic
Start / End / Timeoutexample, and see exactly why 0ms never means 0ms.Read the full lesson -> [link]
#JavaScript #EventLoop #AsyncJS #InterviewPrep #Frontend #CodingInterview #WebDevelopment
What You'll Learn
- Why single-threaded JavaScript can still do async work without blocking
- The exact rule the Event Loop follows when moving tasks to the call stack
- Why
setTimeout(fn, 0)is never actually zero milliseconds
Why the Event Loop Exists
JavaScript is single-threaded — it has only one call stack, executing one thing at a time. So how does it handle setTimeout, API calls, and DOM events without blocking?
The answer: the Event Loop.
Think of it like a restaurant with one chef (the call stack). The chef can only cook one dish at a time. But the restaurant also has a microwave (Web API for timers), a delivery service (Web API for fetch), and a dishwasher (Web API for DOM events). These run independently. When they finish, they put the result on a counter (the queue). The chef picks up the next item from the counter only when the current dish is done.
The Event Loop is the waiter who constantly checks: "Is the chef free? Is there something on the counter? Let me pass it over."
How the Event Loop Works
+-----------------------------------+
| CALL STACK |
| (executes JS code, one at a |
| time, top to bottom) |
+---------------+-------------------+
|
| When async operation found
| (setTimeout, fetch, DOM event)
v
+-----------------------------------+
| WEB APIs |
| (browser handles these in |
| separate threads) |
+---------------+-------------------+
|
| When operation completes,
| callback moves to a queue
v
+-----------------------------------+
| MICROTASK QUEUE (Priority!) | <- Promise.then, MutationObserver
+-----------------------------------+
| CALLBACK QUEUE (Macrotask) | <- setTimeout, setInterval, I/O
+---------------+-------------------+
|
| EVENT LOOP checks:
| "Is call stack empty?"
| -> If yes, push next task
v
Back to CALL STACK
The Core Rule
The Event Loop will ONLY push a task from the queue to the call stack when the call stack is completely empty.
This is why setTimeout(fn, 0) does NOT mean "run immediately." It means "run as soon as the call stack is empty AND all microtasks are done."
Code Example — The Classic
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
Output:
Start
End
Timeout
Step-by-step:
console.log("Start")-> pushed to call stack -> executes -> prints "Start"setTimeout(callback, 0)-> pushed to call stack -> registers callback with Web API -> timer completes instantly -> callback moves to Callback Queueconsole.log("End")-> pushed to call stack -> executes -> prints "End"- Call stack is now empty -> Event Loop checks queues -> finds callback in Callback Queue -> pushes it to call stack
- Callback executes -> prints "Timeout"
Why setTimeout(0) Is Never Actually 0ms
The HTML spec mandates a minimum delay of ~4ms for nested setTimeout calls (after 5 levels of nesting). But even without that, the callback must wait for:
- The current synchronous code to finish
- All microtasks to complete
- Its turn in the callback queue
console.log("Before setTimeout");
setTimeout(() => {
console.log("Inside setTimeout");
}, 0);
// Simulate heavy synchronous work
for (let i = 0; i < 1000000000; i++) {
// blocking the call stack for ~1-2 seconds
}
console.log("After heavy work");
Output:
Before setTimeout
After heavy work <- after ~1-2 seconds of blocking
Inside setTimeout <- only NOW, after call stack is empty
Even though setTimeout was set to 0ms, it waited over a second because the call stack was busy with the for loop.
Common Mistakes
- Believing
setTimeout(fn, 0)schedules the callback for "the next instruction." It actually schedules the callback for "after the stack is empty AND all microtasks have drained" — which can be milliseconds or minutes later. - Treating the Event Loop as something that runs "in parallel" with your code. It only runs when the call stack is empty — any synchronous code in flight blocks it completely.
- Assuming the Event Loop is part of the JS engine (V8). It is actually part of the runtime (the browser or Node.js) — V8 by itself has no event loop.
Interview Questions
Q: What is the Event Loop in JavaScript?
The Event Loop is a mechanism that monitors the call stack and task queues. When the call stack is empty, it takes the first task from the microtask queue (higher priority) or the callback queue (lower priority) and pushes it onto the call stack for execution. It's what allows JavaScript to perform non-blocking asynchronous operations despite being single-threaded.
Q: Why does setTimeout(fn, 0) not execute immediately?
Because
setTimeout(fn, 0)doesn't mean "execute now." It means "add this callback to the Callback Queue after 0ms." The callback still has to wait for: (1) the current synchronous code to finish, (2) all microtasks to complete, and (3) its turn in the queue. The actual minimum delay is also ~4ms due to browser spec clamping.
Q: Is JavaScript synchronous or asynchronous?
JavaScript itself is synchronous and single-threaded — it executes one operation at a time on the call stack. However, the runtime environment (browser or Node.js) provides Web APIs that handle async operations in separate threads. The Event Loop bridges these two worlds, making JavaScript appear asynchronous.
Q: Why does one heavy for loop freeze the whole page?
Because rendering, input handling, timers, and network callbacks all depend on the Event Loop — and the Event Loop can only run when the call stack is empty. A long-running synchronous loop keeps the stack occupied, so no queued task (including repaint) runs until it finishes.
Quick Reference — Cheat Sheet
EVENT LOOP — QUICK MAP
Single-threaded JS:
- ONE call stack
- ONE instruction at a time
Non-blocking magic:
async op -> handed to Web APIs (separate threads)
callback -> queued when op finishes
Event Loop -> pushes callback to stack ONLY when stack is empty
The Core Rule:
if (callStack.isEmpty()) {
drainAllMicrotasks();
pickOneMacrotask();
}
setTimeout(fn, 0) timeline:
sync code -> microtasks -> fn
(NOT "run now")
Previous: Module Scope & ES6 Modules Next: Web APIs & Node APIs -> The Runtime's Toolbox
This is Lesson 3.1 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.