Skip to content
intermediate Phase 5 · JavaScript Async & DOM

Event Loop & Concurrency Model

Understand the event loop, call stack, task queue, microtasks, and the single-threaded concurrency model.

1h
0 problems
Topic Progress 0%

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?

Question 1 options

2. What does code after await actually wait on?

Question 2 options

3. Why does a long synchronous loop freeze the page?

Question 3 options

Flashcards

Question

Microtasks

Answer

Promise callbacks, queueMicrotask, MutationObserver — drained fully after each macrotask.

Question

Macrotasks

Answer

setTimeout, setInterval, I/O, message events — one runs per loop turn.

Question

await semantics

Answer

Pre-await runs sync; post-await resumes as a microtask on settlement.

Question

Starvation rule

Answer

Endlessly queued microtasks block the next macrotask — yield deliberately.

Question

Node concurrency

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.

Continue Learning