Free lesson · JavaScript Basic to Advance
The Event Loop, Visually
The Mechanism That Makes Async Work
The event loop is what bridges JavaScript's single thread to the async work happening around it. It's the most important runtime concept in JavaScript. Understanding it is what separates intermediate JavaScript developers from senior ones, and it's the most-asked deep async question in interviews.
This Topic is going to be longer than most, because the event loop is the conceptual peak of the entire async section. Spend time here.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
Predict the output. The answer is 1, 4, 3, 2 — not 1, 2, 3, 4 and not 1, 3, 2, 4. The reason involves three distinct queues and a very specific priority order. By the end of this Topic, you should be able to trace any code like this step by step.
The Three Players
JavaScript's async execution involves three structures:
The call stack. Where currently executing code lives. When you call a function, its execution context is pushed onto the stack. When it returns, the context is popped off. Topic 6.1 covered this.
The task queue (also called macrotask queue). Where callbacks from setTimeout, setInterval, I/O events, and similar operations wait their turn. Each item in this queue is a function ready to run.
The microtask queue. Where callbacks from Promises (.then, .catch, .finally) and queueMicrotask wait. This queue has higher priority than the task queue — we'll see why in a moment.
graph TD
A[Call Stack] --> B[Currently executing code]
C[Microtask Queue] --> D[Promise callbacks<br/>queueMicrotask]
E[Task Queue] --> F[setTimeout, setInterval<br/>I/O callbacks, UI events]
G[Event Loop] -.->|checks when stack is empty| C
G -.->|then checks| E
style A fill:#7c3aed,color:#fff
style C fill:#10b981,color:#fff
style E fill:#3b82f6,color:#fff
style G fill:#f59e0b,color:#fff
This diagram shows the three structures involved in JavaScript's async execution. The call stack holds currently executing code. The microtask queue holds callbacks from Promises and queueMicrotask. The task queue holds callbacks from setTimeout, setInterval, I/O operations, and UI events. The event loop is a separate mechanism that watches the call stack and the queues. When the call stack is empty, it pulls items from the queues to execute, always draining the microtask queue completely before pulling any task from the task queue.
The Event Loop's Job
Interactive animation: Stack, browser APIs and task queue animated - the loop pushes the oldest waiting task once the stack is empty.
The event loop has a simple job, repeated forever:
- Is there code on the call stack? If yes, let it run.
- When the stack is empty, are there microtasks in the microtask queue? Run them all until the microtask queue is empty.
- Is there a task in the task queue? If yes, take the first one and run it.
- After running one task, check the microtask queue again. Run all new microtasks.
- Repeat.
The crucial points:
- The microtask queue gets fully drained between every task — not just between cycles, but between every individual task. New microtasks added during processing are also drained before the next task.
- The call stack must be empty before either queue gets attention. As long as your synchronous code is running, async callbacks wait.
- The microtask queue has priority over the task queue. All microtasks run before any task.
These three rules are what produce the output ordering we saw at the top of this Topic.
Tracing the Opening Example
Let's trace through the example carefully:
console.log("1"); // sync — runs first
setTimeout(() => console.log("2"), 0); // schedules a TASK
Promise.resolve().then(() => console.log("3")); // schedules a MICROTASK
console.log("4"); // sync — runs second
Step by step:
console.log("1")runs synchronously. Output:1. Stack empties.setTimeout(..., 0)schedules() => console.log("2")as a task. The task queue now has it.Promise.resolve().then(...)schedules() => console.log("3")as a microtask. The microtask queue now has it.console.log("4")runs synchronously. Output:4. Stack empties.- All synchronous code is done. The script has finished running its top-level code.
- Event loop checks the microtask queue first. Finds the Promise callback. Runs it. Output:
3. Microtask queue empties. - Event loop checks the task queue. Finds the setTimeout callback. Runs it. Output:
2. Task queue empties.
Output order: 1, 4, 3, 2. The synchronous code runs first. Then microtasks. Then tasks. Each section completes before the next begins.
Interactive animation: A promise callback jumps ahead of a matured timer callback - something outranks the task queue.
Why the Order Matters
The microtask-before-task priority is a deliberate design choice. It means Promise resolutions happen "as soon as possible after the current code" — without the delays that setTimeout-style scheduling introduces.
console.log("sync 1");
Promise.resolve().then(() => console.log("microtask 1"));
setTimeout(() => {
console.log("task 1");
Promise.resolve().then(() => console.log("microtask 2"));
console.log("task 1 still");
}, 0);
setTimeout(() => console.log("task 2"), 0);
Promise.resolve().then(() => console.log("microtask 3"));
console.log("sync 2");
The careful trace:
- Synchronous code runs first:
sync 1,sync 2. - Stack is empty. Drain microtask queue:
microtask 1,microtask 3. Microtask queue is empty. - Run one task from the task queue: the first setTimeout callback. It logs
task 1, schedules a new microtask, then logstask 1 still. Task done. - Stack is empty. Drain microtask queue:
microtask 2. Microtask queue is empty. - Run next task:
task 2. Task done.
Look at how microtask 2 runs between the two tasks, even though it was scheduled from inside task 1. The microtask queue is checked between every task, not just at the start.
Practical Consequences
The microtask priority has real consequences for code behavior.
Promises are "fast" compared to setTimeout. Even setTimeout(fn, 0) doesn't run before any pending Promises. If you have logic that depends on "as soon as the current synchronous work finishes," Promise.resolve().then(...) happens sooner than setTimeout(..., 0).
Long microtask chains can starve tasks. If you keep adding new microtasks from inside Promise callbacks, the task queue might never get attention. This is a real concern in some applications — a recursive Promise chain can prevent the page from responding to UI events.
// ⚠️ This would starve the task queue
function infinitePromise() {
Promise.resolve().then(() => infinitePromise());
}
// infinitePromise(); // would freeze the page — never lets tasks run
The microtask queue keeps getting new items from inside the callbacks, so the event loop never gets to check the task queue. Don't do this in real code.
Browser rendering happens between tasks, not between microtasks. The browser is allowed to update the screen, run animations, and handle UI events between tasks — but not in the middle of a microtask batch. If your code schedules a long chain of microtasks, the UI can become unresponsive.
Interactive animation: One turn of the loop - run one task to completion, then a paint window, then the next task.
Where Each Thing Lives
Here's a cheat sheet for which queue each common async operation uses:
Tasks (task queue):
setTimeoutcallbackssetIntervalcallbackssetImmediatecallbacks (Node.js)- I/O operation callbacks
- UI events (clicks, key presses, etc.)
MessageChannelcallbacks
Microtasks (microtask queue):
.then,.catch,.finallycallbacks on Promisesawaitcontinuations (Promises underneath)queueMicrotaskcallbacksMutationObservercallbacks (in browsers)
If a question asks "in what order does this execute?", figure out which queue each callback lives in, and apply the priority rules.
The Node.js Event Loop Is Slightly Different
The model above is the standard one used in browsers. Node.js has a similar event loop with a few additional phases — specifically, it distinguishes between things like immediate callbacks, timer callbacks, and I/O callbacks more carefully. The basic rules — microtasks before tasks, synchronous code first — still apply. The phase distinctions matter mostly for senior Node.js interview questions.
For most JavaScript work and most interview questions, the browser model is the one to know.
Common Pitfalls
Warning: The event loop traps.
Assuming
setTimeout(fn, 0)runs immediately. It doesn't. It schedules a task, which only runs after all sync code is done and all pending microtasks are drained. The "0" is a minimum delay, not an actual one.Confusing microtask priority with parallelism. Microtasks don't run in parallel with tasks. They run before the next task, on the same single thread. The "priority" is about ordering, not concurrency.
Starving the task queue with long microtask chains. Be careful with recursive Promise chains. If you don't yield to tasks occasionally, UI events and I/O callbacks get delayed.
Interactive animation: A 100ms timer fires after 302ms because a busy loop held the stack - its delay is a minimum.
Why This Matters in Interviews
Interview: The event loop is the most-asked deep JavaScript interview question. Almost every senior-level interview probes it in some form — either as "explain the event loop" or as an output-prediction puzzle with mixed
setTimeout,Promise.resolve, and synchronous code.The expected senior-level answer covers: the call stack, the two queues (microtask and task), the priority of microtasks over tasks, the rule that microtasks drain completely between tasks, and the fact that this is what makes JavaScript single-threaded without being blocking.
Topic 9.11 — Interview Lens: The Event Loop Question — is the full interview-prep treatment of this material. The most important Interview Lens Topic in the course.
Try It
Exercise: Predict the exact output order of this code before running it:
console.log("A"); setTimeout(() => { console.log("B"); Promise.resolve().then(() => console.log("C")); }, 0); Promise.resolve().then(() => { console.log("D"); setTimeout(() => console.log("E"), 0); }); console.log("F");Six things to log. Trace each through the rules: synchronous first, then microtask drain, then one task, then microtask drain again, then next task. Once you can do this exercise reliably, you've mastered the event loop. The interview question becomes mechanical.