The Event Loop, Stack & Queue
JS single-threaded — তবু সব কিছু একসাথে চলে কীভাবে?
1. JS Has One Thread, One Stack
JavaScript executes one expression at a time on a single call stack. There is no parallelism inside your script — yet a JS app can fetch data, animate, accept clicks and run timers all at once. The trick is that the language is single-threaded; the runtime (browser or Node) hands long-running work to native code and feeds the results back through queues.
2. The Picture
3. The Famous Puzzle
Predict the output before running it.
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
Output: A, D, C, B. Sync first (A, D). Then microtasks drain — Promise.then runs (C). Only after every microtask is done does one macrotask (the setTimeout callback B) run.
4. setTimeout(fn, 0) Is Not Zero
Browsers clamp nested timers to a minimum of ~4ms. Even at the top level, the callback runs only after the stack empties and microtasks drain — it's never truly synchronous.
const t0 = Date.now();
setTimeout(() => {
console.log("elapsed:", Date.now() - t0, "ms");
}, 0);
// Block stack briefly
for (let i = 0; i < 5e6; i++);
console.log("sync done");
5. Microtasks Always Drain First
setTimeout(() => console.log("macro 1"), 0);
Promise.resolve().then(() => {
console.log("micro 1");
Promise.resolve().then(() => console.log("micro 2"));
});
setTimeout(() => console.log("macro 2"), 0);
console.log("sync");
Order: sync → micro 1 → micro 2 → macro 1 → macro 2. Even nested microtasks run before the next macrotask.
6. Why a Long Sync Loop Freezes the Page
Browsers also queue user events (clicks, scroll) as macrotasks. While your sync code runs, nothing else can. That's why you should never block the main thread with heavy work — break it up with setTimeout, requestAnimationFrame, or push it to a Web Worker.
7. Glossary (শব্দকোষ)
| Term | Meaning | বাংলায় |
|---|---|---|
| Call stack | LIFO stack of function calls JS is currently executing. | চলমান function call-গুলোর LIFO stack। |
| Single-threaded | One thread of JS execution per realm. | প্রতি realm-এ JS-এর একটিই thread। |
| Web/Node API | Native code that does I/O (timers, fetch, fs) on other threads. | I/O করার native code — অন্য thread-এ চলে। |
| Macrotask | Tasks like setTimeout callbacks, I/O, UI events. | setTimeout, I/O, UI event-এর মতো task। |
| Microtask | Promise .then / queueMicrotask — drained between macrotasks. | Promise .then ও queueMicrotask; প্রতি macrotask-এর আগে drain হয়। |
| Event loop | The cycle that empties the stack, drains microtasks, takes one macrotask. | Stack খালি → microtask drain → এক macrotask — এই চক্র। |
queueMicrotask | Schedules a microtask without using a Promise. | Promise ছাড়াই microtask schedule করার API। |
| Starvation | Macrotasks delayed because microtasks keep producing more microtasks. | Microtask আবার microtask তৈরি করায় macrotask পিছিয়ে যাওয়া। |
setTimeout(0) কখনোই আগে চলে না — Promise.then সর্বদা আগে। Long sync work UI freeze করে — Worker-এ পাঠান বা requestAnimationFrame-এ chunk করুন।
8. Practice Problems
- Predict and run the famous A–D–C–B puzzle.
✨ Show Answer
a1.jsconsole.log("A"); setTimeout(() => console.log("B"), 0); Promise.resolve().then(() => console.log("C")); console.log("D");Output:
A D C B. - Show that
queueMicrotaskruns before any setTimeout(0).✨ Show Answer
a2.jssetTimeout(() => console.log("timeout"), 0); queueMicrotask(() => console.log("micro")); console.log("sync"); - Predict: what does
Promise.resolve(42).then(v => v + 1).then(console.log)print?✨ Show Answer
a3.jsPromise.resolve(42).then(v => v + 1).then(console.log); - Demonstrate that a long sync loop blocks a setTimeout.
✨ Show Answer
a4.jsconst t0 = Date.now(); setTimeout(() => console.log("timer:", Date.now() - t0), 0); for (let i = 0; i < 2e7; i++); console.log("sync done", Date.now() - t0); - Show that nested microtasks all run before the next macrotask.
✨ Show Answer
a5.jssetTimeout(() => console.log("timeout"), 0); Promise.resolve().then(() => { console.log("m1"); Promise.resolve().then(() => Promise.resolve().then(() => console.log("m3"))); Promise.resolve().then(() => console.log("m2")); }); - Build a tiny "task queue" simulator that runs one item per tick using setTimeout(0).
✨ Show Answer
a6.jsconst tasks = [1, 2, 3, 4, 5]; function tick() { if (!tasks.length) return console.log("done"); console.log("task", tasks.shift()); setTimeout(tick, 0); } tick(); - In one sentence, why is JS called "non-blocking" if it's single-threaded?
✨ Show Answer
Answer: Because long-running operations (network, timers, I/O) are handed to native APIs that run on other threads, leaving the JS thread free to keep handling synchronous work; results come back as queued tasks for later.
- Predict: how many microtasks run before the first setTimeout if you chain
.then5 times?✨ Show Answer
All 5 — each
.thenqueues another microtask, and the loop drains the entire microtask queue before any macrotask. Run the snippet to confirm. - Compare event loop in browser vs Node.js (one paragraph).
✨ Show Answer
Answer: Both have a microtask queue and process it the same way. The browser's macrotask queue is a single queue of user events, timers, and rendering tasks. Node's libuv loop has multiple phases (timers, I/O callbacks, check, close) and runs microtasks between phases, so the relative order of timers vs immediates can differ from browsers — but the core "drain microtasks, then take one macrotask" rule is identical.
- Use
queueMicrotaskto schedule a callback after the current sync code.✨ Show Answer
a10.jsqueueMicrotask(() => console.log("after sync")); console.log("sync"); - Why might a tight while-loop with
setTimeout(0)still freeze the page in some cases?✨ Show Answer
Answer: Each setTimeout body becomes a macrotask, but if you keep queueing them in a loop without yielding (or if browsers clamp nested timers to 4ms), the queue grows faster than the loop can render. Use
requestAnimationFrameor actual chunked work viarequestIdleCallbackfor cooperative scheduling. - Show that a Promise resolved synchronously still settles asynchronously.
✨ Show Answer
a12.jsconsole.log("before"); Promise.resolve("hi").then(console.log); console.log("after");Output:
before, after, hi..thenalways defers to a microtask.
Summary — Module 20
JS itself is one thread on one stack. The runtime offloads I/O to native APIs and feeds results back through two queues — microtasks (Promises, queueMicrotask) and macrotasks (timers, I/O, UI). The event loop drains all microtasks between every macrotask. Long sync work freezes the UI; break it up or move it to a Worker.