Chapter 13 β Event Loop & Task Queues (Visual Guide)
π Definition
JavaScript runs your code on one thread β it can do only one thing at a time. The event loop is the manager that decides what runs next: your normal code first, then waiting callbacks from two queues β the microtask queue (promises) and the task queue (timers, clicks, network events).
One line to remember: Run the current code β run ALL microtasks β (browser paints) β run ONE task β repeat.
π³ A Simple Analogy β One Chef in a Kitchen
| Kitchen | JavaScript |
|---|---|
| One chef who can cook only one dish at a time | Call stack β runs one function at a time |
| Kitchen helpers: oven timer, delivery person | Web APIs / Node APIs β timers, network, file system work in the background |
| VIP order board β always served first, all of them | Microtask queue β promise callbacks, await |
| Regular order board β one order at a time | Task (macrotask) queue β setTimeout, clicks, I/O |
| Manager who tells the chef what to cook next, only when the chef is free | Event loop |
The chef never stops in the middle of a dish. When a dish is finished, the manager first hands over every VIP order, and only then one regular order.
πΌοΈ The Big Picture
π The Five Parts
| Part | What it is | Examples |
|---|---|---|
| Call stack | Where functions run. Last in, first out. | Your functions, console.log |
| Web APIs / Node APIs | Work done outside JavaScript, in the background | Timers, fetch, DOM events, file reading |
| Microtask queue | High-priority callbacks. All of them run before anything else. | .then/.catch/.finally, code after await, queueMicrotask |
| Task (macrotask) queue | Normal callbacks. One runs per loop turn. | setTimeout, setInterval, clicks, network events, setImmediate (Node) |
| Event loop | Keeps checking: "Is the stack empty? Then what's next?" | β |
π One Turn of the Event Loop
The rules in words:
- Run the current script (or task) until the call stack is empty. Nothing can interrupt it.
- Run every microtask. If a microtask adds another microtask, that runs too, before moving on.
- The browser may repaint the screen (and run
requestAnimationFramecallbacks). - Take one task from the task queue and go back to step 1.
π» Example 1 β The Classic Question
console.log("1: start");
setTimeout(() => console.log("2: timeout"), 0);
Promise.resolve().then(() => console.log("3: promise"));
console.log("4: end");Output: 1: start β 4: end β 3: promise β 2: timeout
Step by step
| Step | What happens | Call stack | Microtask queue | Task queue | Output so far |
|---|---|---|---|---|---|
| 1 | Run line 1 | log("1") |
β | β | 1 |
| 2 | setTimeout hands the timer to the browser (0 ms). When it expires, its callback joins the task queue |
setTimeout |
β | timeout cb |
1 |
| 3 | Promise is already resolved β its .then callback joins the microtask queue |
then |
promise cb |
timeout cb |
1 |
| 4 | Run line 4 | log("4") |
promise cb |
timeout cb |
1 4 |
| 5 | Script finished β stack empty β run all microtasks | promise cb |
β | timeout cb |
1 4 3 |
| 6 | Microtasks empty β take one task | timeout cb |
β | β | 1 4 3 2 |
Key insight:
setTimeout(fn, 0)does not mean "run now". It means "run after at least 0 ms, once the stack is empty and all microtasks are done".
π» Example 2 β async / await
async function load() {
console.log("A1: inside load");
await null; // everything after await becomes a microtask
console.log("A2: after await");
}
console.log("S: start");
setTimeout(() => console.log("T: timeout"), 0);
load();
Promise.resolve().then(() => console.log("P: promise"));
console.log("E: end");Output: S β A1 β E β A2 β P β T
| Step | Event | Microtask queue | Task queue | Output |
|---|---|---|---|---|
| 1 | S logs |
β | β | S |
| 2 | Timer registered | β | T | S |
| 3 | load() runs synchronously until await β A1 logs |
β | T | S A1 |
| 4 | await pauses load; the rest of the function is queued |
A2 | T | S A1 |
| 5 | .then queued |
A2, P | T | S A1 |
| 6 | E logs, script ends |
A2, P | T | S A1 E |
| 7 | Run all microtasks in order | β | T | S A1 E A2 P |
| 8 | Run one task | β | β | S A1 E A2 P T |
An
asyncfunction runs normally (synchronously) until the firstawait. Only the code afterawaitis delayed.
π» Example 3 β Microtasks Added by Microtasks
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => {
console.log("p1");
Promise.resolve().then(() => console.log("p2")); // added while microtasks are running
});Output: p1 β p2 β timeout
New microtasks join the same queue that is being emptied, so p2 still runs before the timeout.
π» Example 4 β Microtasks Run After Each Task
setTimeout(() => {
console.log("t1");
Promise.resolve().then(() => console.log("promise inside t1"));
}, 0);
setTimeout(() => console.log("t2"), 0);Output: t1 β promise inside t1 β t2
After task t1, the loop empties the microtask queue before picking task t2.
π Which Queue Does It Go To?
| API | Queue |
|---|---|
Promise.then / catch / finally |
π’ Microtask |
Code after await |
π’ Microtask |
queueMicrotask(fn) |
π’ Microtask |
MutationObserver |
π’ Microtask |
setTimeout, setInterval |
π Task |
| DOM events (click, keydown, input) | π Task |
| Network / I/O callbacks | π Task |
MessageChannel / postMessage |
π Task |
setImmediate (Node only) |
π Task (check phase) |
requestAnimationFrame |
π¨ Before the next paint (render step) |
new Promise(executor) β the executor itself |
β‘ Runs immediately (synchronous!) |
process.nextTick (Node only) |
π£ Its own queue, runs even before promise microtasks |
β οΈ Common Mistakes
1. Blocking the call stack freezes everything
button.addEventListener("click", () => {
const end = Date.now() + 3000;
while (Date.now() < end) {} // 3 s of busy work β page can't scroll, click or repaint
console.log("done");
});While the stack is busy, the event loop can't run anything β no clicks, no timers, no painting.
Fix: split work into chunks so the loop can breathe, or move it to a Web Worker (browser) / worker thread (Node).
async function processInChunks(items, chunkSize = 500) {
for (let i = 0; i < items.length; i += chunkSize) {
items.slice(i, i + chunkSize).forEach(processItem);
await new Promise((r) => setTimeout(r, 0)); // give the event loop a turn
}
}2. Endless microtasks also freeze the page
function loop() { Promise.resolve().then(loop); } // β microtask queue never empties
loop(); // page never repaints, timers never run3. Expecting setTimeout(fn, 1000) to run in exactly 1 second
The delay is a minimum. If the stack is busy for 3 seconds, the callback runs after 3 seconds. Browsers also clamp nested timers to at least ~4 ms.
4. Thinking new Promise is async
new Promise((resolve) => {
console.log("executor"); // runs immediately
resolve();
}).then(() => console.log("then"));
console.log("after");
// executor β after β thenπ© Node.js Is Slightly Different
Node uses the same idea but its event loop has phases: timers β pending callbacks β poll (I/O) β check (setImmediate) β close callbacks. Between callbacks it drains the process.nextTick queue first, then promise microtasks. Details are in the Node.js: Event Loop chapter.
π§© Practice β Predict the Output
Puzzle 1
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
queueMicrotask(() => console.log(4));
console.log(5);Show answer
1 5 3 4 2 β sync logs first (1, 5), then both microtasks in the order they were queued (3, 4), then the timer task (2).
Puzzle 2
async function f() {
console.log("f1");
await g();
console.log("f2");
}
async function g() { console.log("g"); }
f();
console.log("end");Show answer
f1 g end f2 β f runs synchronously and calls g (also synchronous inside), then await pauses f; end logs; finally the rest of f runs as a microtask.
Puzzle 3
setTimeout(() => console.log("A"), 10);
setTimeout(() => console.log("B"), 0);
const start = Date.now();
while (Date.now() - start < 50) {} // block for 50 ms
console.log("C");Show answer
C B A β nothing runs until the 50 ms loop ends. By then both timers have expired; B's timer (0 ms) expired before A's (10 ms), so B runs first.
Puzzle 4
const p = new Promise((resolve) => {
console.log("1");
setTimeout(() => { console.log("2"); resolve(); }, 0);
});
p.then(() => console.log("3"));
console.log("4");Show answer
1 4 2 3 β the executor runs immediately (1), then 4. The promise isn't resolved yet, so no microtask exists. The timer task logs 2 and resolves the promise, which queues the .then microtask, and that runs right after the task (3).
Puzzle 5
button.addEventListener("click", () => {
Promise.resolve().then(() => console.log("micro"));
console.log("listener");
});
button.click(); // called from code
console.log("after click");Show answer
listener after click micro β button.click() from script calls the listener synchronously, so the stack isn't empty until after "after click"; only then do microtasks run. (A real user click would print listener micro because each user-triggered listener runs as its own task.)
π Real-World Impact
- Understanding the loop explains why a heavy
forloop makes the UI freeze, and why moving it to a Web Worker fixes it. - It explains why React state updates and promise callbacks appear "later" than surrounding code.
- In Node.js APIs, one CPU-heavy request (big JSON parse, image resize) blocks all users β the same rule applies on the server.
π― Likely Interview Questions
- What is the event loop? β A mechanism that runs queued callbacks when the call stack is empty: all microtasks first, then one task, repeatedly.
- Microtask vs macrotask? β Microtasks (promises,
await,queueMicrotask) all run right after the current code; macrotasks (setTimeout, events, I/O) run one per loop turn. - Why does
Promise.thenrun beforesetTimeout(fn, 0)? β The microtask queue is emptied before the next task is taken. - Is JavaScript single-threaded? Then how is it asynchronous? β Your JS runs on one thread; timers, network and I/O run in the browser/Node background, and their callbacks are queued for the loop.
- Does
setTimeout(fn, 0)run immediately? β No; after the current code and all microtasks finish, with at least the given delay. - What runs synchronously in an
asyncfunction? β Everything up to the firstawait. - How can the event loop get blocked? β Long synchronous work or an endless chain of microtasks; nothing else can run, the page freezes.
- Where does
requestAnimationFramerun? β In the render step, just before the browser paints. - How is Node's event loop different? β It has phases (timers, poll, checkβ¦) and a
process.nextTickqueue that runs before promise microtasks.