The Single-Threaded Model
The Single-Threaded Model
JavaScript executes on one call stack: one function frame at a time, which is why a long synchronous loop freezes a page or blocks a Node server. Concurrency still exists — timers, I/O, and network responses are handled by the runtime's event sources (Web APIs in browsers, libuv in Node) off the main thread. When work finishes, a callback is queued rather than run immediately.
So "single-threaded" means single execution thread, not single capability: the event loop drains the stack, then services queued work. Blocking the stack does not just slow your code — it delays every queued callback behind it, which is the actual user-visible symptom of expensive JSON parsing or layout thrash on the main thread.
Microtasks and Macrotasks
Microtasks and Macrotasks
The loop follows a strict order: pop one macrotask (timer, I/O, input event) → run it to completion → drain the entire microtask queue → take the next macrotask. Microtasks are promise callbacks, queueMicrotask, and MutationObserver; macrotasks are setTimeout, setInterval, I/O, and message events.
console.log('1');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('2'));
console.log('3');
// output: 1 3 2 4
await splits the function: everything before await runs synchronously; everything after is scheduled as a microtask when the promise settles. That is why await still beats setTimeout(..., 0) even after the value is ready.
Pitfalls and Interview Focus
Pitfalls and Interview Focus
Microtasks that enqueue more microtasks never yield — one runaway promise chain starves the next macrotask (and in browsers, rendering), while setTimeout at least yields between turns. Use setTimeout(fn, 0), MessageChannel, or requestAnimationFrame (UI) when you intentionally need to defer past pending microtasks.
In Node, the event loop is throughput, not parallelism: CPU-bound work needs worker threads, while I/O concurrency stays cheap because the runtime waits off-thread. In browsers, long tasks inflate INP/LCP because input handling sits behind the stack.
Interview: answer ordering questions by classifying each statement — sync, microtask, macrotask — then emit in loop order. State the drain rule ("all microtasks after every macrotask") explicitly; it is the whole grade.
Quiz
1. After one macrotask finishes, when does the loop run microtasks?
2. What does code after await actually wait on?
3. Why does a long synchronous loop freeze the page?
Flashcards
Question
Microtasks
Click to reveal answer
Answer
Promise callbacks, queueMicrotask, MutationObserver — drained fully after each macrotask.
Question
Macrotasks
Click to reveal answer
Answer
setTimeout, setInterval, I/O, message events — one runs per loop turn.
Question
await semantics
Click to reveal answer
Answer
Pre-await runs sync; post-await resumes as a microtask on settlement.
Question
Starvation rule
Click to reveal answer
Answer
Endlessly queued microtasks block the next macrotask — yield deliberately.
Question
Node concurrency
Click to reveal answer
Answer
I/O waits off-thread; CPU work needs worker_threads — event loop ≠ extra cores.
Revision Notes
Key Takeaways
- 1. One macrotask, then every microtask, then the next macrotask
- 2. Blocking the stack blocks everything behind it — including rendering
- 3. await is microtask resumption, which explains classic ordering questions
Interview Tips
- • Classify each line sync/micro/macro before predicting output — state the drain rule out loud
- • Tie blocking to real metrics: long tasks degrade INP; in Node they cap throughput
Cheat Sheet
Loop: pop 1 macrotask → drain ALL microtasks → next macrotask. Micro = promises/queueMicrotask; macro = timers/I/O. await resumes as microtask. Stack blocked = UI frozen; CPU work → worker_threads.