The Event Loop, Stack & Queue

JS single-threaded — তবু সব কিছু একসাথে চলে কীভাবে?

~45 min Advanced 12 practice problems Live runner

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.

JavaScript single-threaded — একসাথে একটিই কাজ। কিন্তু runtime (browser/Node) আলাদা thread-এ I/O চালায় এবং ফলাফল queue-এ ফেরত দেয়। Event loop এই queue থেকে কাজ বেছে আমাদের stack-এ পাঠায়।

2. The Picture

Call Stack main() handler() Web / Node APIs setTimeout · fetch · DOM event native threads do the waiting Microtask Queue Promise.then · queueMicrotask Task (Macrotask) Queue setTimeout cb · I/O · UI events Event Loop if stack empty: drain microtasks → 1 macrotask Figure 20.1 — runtime hands long-running work to native APIs; the event loop pumps results back to the stack.

3. The Famous Puzzle

Predict the output before running it.

order.js
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.

Order: A → D → C → B। প্রথমে sync, তারপর microtask (Promise), সবশেষে macrotask (setTimeout)। Microtask queue সব শেষ না হলে কোনো macrotask চলে না।

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.

timer.js
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

drain.js
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.

Rule of thumb Aim for tasks under ~50 ms. Anything longer and the user notices stutter. Heavy work goes to a Web Worker (Module 29).

7. Glossary (শব্দকোষ)

TermMeaningবাংলায়
Call stackLIFO stack of function calls JS is currently executing.চলমান function call-গুলোর LIFO stack।
Single-threadedOne thread of JS execution per realm.প্রতি realm-এ JS-এর একটিই thread।
Web/Node APINative code that does I/O (timers, fetch, fs) on other threads.I/O করার native code — অন্য thread-এ চলে।
MacrotaskTasks like setTimeout callbacks, I/O, UI events.setTimeout, I/O, UI event-এর মতো task।
MicrotaskPromise .then / queueMicrotask — drained between macrotasks.Promise .then ও queueMicrotask; প্রতি macrotask-এর আগে drain হয়।
Event loopThe cycle that empties the stack, drains microtasks, takes one macrotask.Stack খালি → microtask drain → এক macrotask — এই চক্র।
queueMicrotaskSchedules a microtask without using a Promise.Promise ছাড়াই microtask schedule করার API।
StarvationMacrotasks delayed because microtasks keep producing more microtasks.Microtask আবার microtask তৈরি করায় macrotask পিছিয়ে যাওয়া।
মনে রাখুন: JS single-threaded — কিন্তু runtime native API দিয়ে I/O আলাদা thread-এ চালায়। Stack খালি হলে microtask queue পুরোটা drain হয়, তারপর এক macrotask চলে। তাই setTimeout(0) কখনোই আগে চলে না — Promise.then সর্বদা আগে। Long sync work UI freeze করে — Worker-এ পাঠান বা requestAnimationFrame-এ chunk করুন।

8. Practice Problems

  1. Predict and run the famous A–D–C–B puzzle.
    ✨ Show Answer
    a1.js
    console.log("A");
    setTimeout(() => console.log("B"), 0);
    Promise.resolve().then(() => console.log("C"));
    console.log("D");

    Output: A D C B.

  2. Show that queueMicrotask runs before any setTimeout(0).
    ✨ Show Answer
    a2.js
    setTimeout(() => console.log("timeout"), 0);
    queueMicrotask(() => console.log("micro"));
    console.log("sync");
  3. Predict: what does Promise.resolve(42).then(v => v + 1).then(console.log) print?
    ✨ Show Answer
    a3.js
    Promise.resolve(42).then(v => v + 1).then(console.log);
  4. Demonstrate that a long sync loop blocks a setTimeout.
    ✨ Show Answer
    a4.js
    const 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);
  5. Show that nested microtasks all run before the next macrotask.
    ✨ Show Answer
    a5.js
    setTimeout(() => 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"));
    });
  6. Build a tiny "task queue" simulator that runs one item per tick using setTimeout(0).
    ✨ Show Answer
    a6.js
    const tasks = [1, 2, 3, 4, 5];
    function tick() {
        if (!tasks.length) return console.log("done");
        console.log("task", tasks.shift());
        setTimeout(tick, 0);
    }
    tick();
  7. 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.

  8. Predict: how many microtasks run before the first setTimeout if you chain .then 5 times?
    ✨ Show Answer

    All 5 — each .then queues another microtask, and the loop drains the entire microtask queue before any macrotask. Run the snippet to confirm.

  9. 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.

  10. Use queueMicrotask to schedule a callback after the current sync code.
    ✨ Show Answer
    a10.js
    queueMicrotask(() => console.log("after sync"));
    console.log("sync");
  11. 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 requestAnimationFrame or actual chunked work via requestIdleCallback for cooperative scheduling.

  12. Show that a Promise resolved synchronously still settles asynchronously.
    ✨ Show Answer
    a12.js
    console.log("before");
    Promise.resolve("hi").then(console.log);
    console.log("after");

    Output: before, after, hi. .then always 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.

JS single-thread; native API আলাদা thread-এ wait করে। Microtask queue সব শেষ হলে তবেই একটি macrotask চলে। Long sync work UI freeze করে।

Next Module → Callbacks & Callback Hell — পুরোনো async এবং তার সমস্যা।