JavaScript · Syntax · Advanced

The event loop

10 tasks

How JavaScript handles concurrency with one thread and one queue at a time. Covers the call stack, task queue, microtask queue, and the order of `setTimeout` vs `Promise.resolve`.

Why single-threaded, call stack, task queue, microtask queue

#
**Why JavaScript is single-threaded** JavaScript was designed for the browser where it touches the DOM. If two threads could modify the DOM simultaneously, you'd need locks and the mental overhead that comes with them. Single-threading avoids all that — there are no data races, no deadlocks, no need for mutexes. Concurrency is achieved differently: by *not blocking* on I/O. When the browser makes a network request, it hands it to the OS and *registers a callback* to be called when the data arrives. Meanwhile the JS thread is free to handle UI events, run animations, respond to user input. This is *non-blocking I/O*. **The event loop model** There is one call stack. It runs one frame at a time to completion. Callbacks and microtasks queue up and are processed between stack frames: ``` Call Stack Microtask Queue Task Queue (Macrotasks) ────────── ─────────────── ────────────────────── │ fn() │ ←── │ .then() │ ←─ │ setTimeout cb │ │ main() │ │ await │ │ I/O callback │ ────────── │ queueMicro │ │ setInterval cb │ ─────────────── ────────────────────── ``` **The full loop — step by step:** 1. Run the current task (synchronous code) to completion 2. Drain the microtask queue completely (new microtasks added during drain also run *before* the next macrotask) 3. Render (browser only, if a frame is due) 4. Pick the next macrotask from the task queue 5. Repeat **Macrotasks** (task queue): `setTimeout`, `setInterval`, I/O callbacks, UI events, `MessageChannel` **Microtasks** (microtask queue): Promise `.then`/`.catch`/`.finally`, `await` resume, `queueMicrotask()`, `MutationObserver`

Ordering examples, async/await, microtask-in-macrotask, output prediction

#
**Ordering examples** ```js // Pattern: sync → microtasks → macrotasks console.log('A'); // sync setTimeout(() => console.log('D'), 0); // macrotask Promise.resolve().then(() => console.log('C')); // microtask console.log('B'); // sync // Output: A B C D ``` **async/await desugared** `async/await` is syntax sugar for Promises. Each `await` creates a microtask boundary — execution pauses and resumes as a microtask: ```js async function f() { console.log('A'); // runs synchronously when f() is called await Promise.resolve(); console.log('C'); // resumes as a microtask } f(); console.log('B'); // runs before C // Output: A B C ``` **Microtask inside a macrotask** Microtasks queued *inside* a macrotask all run before the next macrotask: ```js setTimeout(() => { console.log('macro 1'); Promise.resolve().then(() => console.log('micro inside macro 1')); }, 0); setTimeout(() => console.log('macro 2'), 0); // Output: macro 1 → micro inside macro 1 → macro 2 ``` **Predicting output — the algorithm** ```js console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve() .then(() => console.log(3)) .then(() => console.log(4)); console.log(5); // Output: 1 5 3 4 2 // // Step 1 (sync): logs 1, schedules setTimeout, chains .then x2, logs 5 // Step 2 (microtasks): .then(3) runs, its .then(4) is queued, then (4) runs // Step 3 (macrotask): setTimeout fires, logs 2 ``` **`queueMicrotask(fn)`** Direct API to schedule a microtask — equivalent to `Promise.resolve().then(fn)` but without creating a Promise object: ```js queueMicrotask(() => console.log('runs before next macrotask')); ```

Quick reference table, pitfalls, await-in-loop, Web Workers

#
**Quick reference** | What | Queue | Priority | |---|---|---| | Synchronous code | current task | highest | | `Promise.then/catch/finally` | microtask | before next macrotask | | `await` resume point | microtask | before next macrotask | | `queueMicrotask()` | microtask | before next macrotask | | `MutationObserver` | microtask | before next macrotask | | `setTimeout(fn, 0)` | macrotask | after all microtasks | | `setInterval` | macrotask (recurring) | after all microtasks | | `requestAnimationFrame` | before-paint task | browser only, ~16ms | | I/O callbacks | macrotask | after all microtasks | **Pitfalls** **1. Heavy sync work blocks everything:** ```js // BAD — freezes the tab for the duration of the loop function processAll(items) { items.forEach(item => heavyWork(item)); } // BETTER — yield to the event loop between chunks async function processAll(items) { for (let i = 0; i < items.length; i++) { heavyWork(items[i]); if (i % 100 === 0) await new Promise(r => setTimeout(r, 0)); } } ``` **2. Infinite microtask recursion starves macrotasks:** ```js // Freezes the tab — microtasks run before the next macrotask // so the event loop never moves on function loop() { Promise.resolve().then(loop); } loop(); // Safe equivalent using macrotask: function loop() { setTimeout(loop, 0); } ``` **3. `setTimeout(fn, 0)` is not zero delay:** The delay is a *minimum*, not a guarantee. The callback fires only when the call stack is empty and the task queue is reached. Under heavy load this could be many milliseconds later. **4. `await` inside a loop:** ```js // Sequential — each fetch waits for the previous: for (const url of urls) { const data = await fetch(url); } // Concurrent — all fetches start at the same time: const results = await Promise.all(urls.map(url => fetch(url))); ``` **Web Workers — true parallelism for CPU work** When you need real CPU parallelism (image processing, parsing), use a Worker. Workers run in a separate thread with their own event loop and communicate via `postMessage`: ```js // main.js const worker = new Worker('./worker.js'); worker.postMessage({ data: largeArray }); worker.onmessage = e => console.log('result:', e.data); // worker.js self.onmessage = e => { const result = heavyCPUWork(e.data.data); // doesn't block main thread self.postMessage(result); }; ```
01

Predict: synchronous call stack

#

Without running it, predict the exact output order of this code: ```js function a() { console.log('a'); b(); } function b() { console.log('b'); } console.log('start'); a(); console.log('end'); ``` Write the expected output as comments, then verify by running it.

function a() { console.log('a'); b(); }
function b() { console.log('b'); }

console.log('start');
a();
console.log('end');

// Expected output (write here before running):
// ???
Solution
function a() { console.log('a'); b(); }
function b() { console.log('b'); }

console.log('start');
a();
console.log('end');

// Output: start, a, b, end
// Stack grows: main -> a -> b, then unwinds: b returns, a returns, main continues
02

setTimeout(0) runs after synchronous code

#

Predict the output order, then explain WHY `'2'` appears after `'3'` even though the delay is 0: ```js console.log('1'); setTimeout(() => console.log('2'), 0); console.log('3'); ```

console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');

// Output order: ???
// Why?
Solution
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');

// Output: 1, 3, 2
//
// setTimeout schedules a macrotask in the task queue.
// The current synchronous code (1, then 3) finishes first.
// Only then does the event loop pick up the queued callback (2).
// '0ms delay' means 'not before 0ms AND after the current task ends'.
03

Promise .then() is a microtask

#

Predict the output. Then compare with the `setTimeout(0)` example from the previous exercise and explain the difference: ```js console.log('1'); Promise.resolve().then(() => console.log('2')); console.log('3'); ```

console.log('1');
Promise.resolve().then(() => console.log('2'));
console.log('3');

// Output: ???
// How is this different from setTimeout(fn, 0)?
Solution
console.log('1');
Promise.resolve().then(() => console.log('2'));
console.log('3');

// Output: 1, 3, 2
// Same order as setTimeout example, but the mechanism differs.
//
// Promise .then() is a MICROTASK.
// Microtasks run after the current task but BEFORE the next macrotask.
// With setTimeout: sync -> macrotask
// With .then():    sync -> microtask -> (then macrotask if any)
// The observable order is the same here, but microtasks have higher priority.
04

Classic ordering: sync + Promise + setTimeout

#

Predict the exact output (4 lines). This is the canonical event-loop ordering test: ```js console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D'); ```

console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');

// Output (4 lines): ???
Solution
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');

// Output: A, D, C, B
//
// Step 1 - sync: A, queue macro(B), queue micro(C), D
// Step 2 - drain microtasks: C
// Step 3 - next macrotask: B
05

Chained .then() and setTimeout interleaving

#

Predict the output: ```js setTimeout(() => console.log('macro'), 0); Promise.resolve() .then(() => console.log('micro 1')) .then(() => console.log('micro 2')) .then(() => console.log('micro 3')); console.log('sync'); ```

setTimeout(() => console.log('macro'), 0);
Promise.resolve()
  .then(() => console.log('micro 1'))
  .then(() => console.log('micro 2'))
  .then(() => console.log('micro 3'));
console.log('sync');

// Output: ???
Solution
setTimeout(() => console.log('macro'), 0);
Promise.resolve()
  .then(() => console.log('micro 1'))
  .then(() => console.log('micro 2'))
  .then(() => console.log('micro 3'));
console.log('sync');

// Output: sync, micro 1, micro 2, micro 3, macro
//
// Each .then() in the chain adds a microtask when the previous resolves.
// The whole microtask queue drains before 'macro' gets a chance to run.
06

async/await suspends at each await

#

Predict the output: ```js async function main() { console.log('async start'); await Promise.resolve(); console.log('async end'); } console.log('before'); main(); console.log('after'); ```

async function main() {
  console.log('async start');
  await Promise.resolve();
  console.log('async end');
}

console.log('before');
main();
console.log('after');

// Output: ???
Solution
async function main() {
  console.log('async start');
  await Promise.resolve();
  console.log('async end');
}

console.log('before');
main();
console.log('after');

// Output: before, async start, after, async end
//
// main() runs synchronously until the first 'await'.
// 'await' suspends main() and schedules the rest as a microtask.
// Execution returns to the call site: 'after' runs.
// Then the microtask queue drains: 'async end' runs.
07

queueMicrotask — explicit microtask scheduling

#

Use `queueMicrotask` to schedule a callback and predict where it appears relative to sync code and `setTimeout(0)`: ```js setTimeout(() => console.log('timeout'), 0); queueMicrotask(() => console.log('microtask')); console.log('sync'); ```

setTimeout(() => console.log('timeout'), 0);
queueMicrotask(() => console.log('microtask'));
console.log('sync');

// Output: ???
Solution
setTimeout(() => console.log('timeout'), 0);
queueMicrotask(() => console.log('microtask'));
console.log('sync');

// Output: sync, microtask, timeout
//
// queueMicrotask() schedules a microtask, just like Promise.resolve().then().
// Microtasks run before any macrotask, so 'microtask' beats 'timeout'.
08

Blocking the event loop

#

Write a function `blockFor(ms)` that synchronously blocks the main thread for `ms` milliseconds using a busy loop. Schedule a `setTimeout` with 50ms, then call `blockFor(200)`. Observe that the timeout fires late — at ~200ms, not 50ms — and explain why.

function blockFor(ms) {
  // busy loop: block the thread for ms milliseconds
}

const t0 = Date.now();
setTimeout(() => {
  console.log(`Fired after ${Date.now() - t0}ms`); // expect ~200ms, not 50
}, 50);

blockFor(200);
console.log(`Sync done at ${Date.now() - t0}ms`);
Solution
function blockFor(ms) {
  const end = Date.now() + ms;
  while (Date.now() < end) {}
}

const t0 = Date.now();
setTimeout(() => {
  console.log(`Fired after ${Date.now() - t0}ms`); // ~200ms
}, 50);

blockFor(200);
console.log(`Sync done at ${Date.now() - t0}ms`); // ~200ms

// While blockFor() is running, the event loop is stuck.
// No timers, no network, no user events can fire — they all queue up.
// The 50ms timeout can only fire after the 200ms block releases the thread.
09

Microtask inside a macrotask

#

Predict the output. Pay attention to the microtask that is scheduled INSIDE the first `setTimeout` callback: ```js setTimeout(() => { console.log('macro 1'); Promise.resolve().then(() => console.log('micro inside')); }, 0); setTimeout(() => console.log('macro 2'), 0); ```

setTimeout(() => {
  console.log('macro 1');
  Promise.resolve().then(() => console.log('micro inside'));
}, 0);
setTimeout(() => console.log('macro 2'), 0);

// Output: ???
Solution
setTimeout(() => {
  console.log('macro 1');
  Promise.resolve().then(() => console.log('micro inside'));
}, 0);
setTimeout(() => console.log('macro 2'), 0);

// Output: macro 1, micro inside, macro 2
//
// The event loop runs ONE macrotask, then drains ALL microtasks, then the next macrotask.
// So: run macro 1 -> .then() queues a microtask -> drain microtasks (micro inside)
//     -> run macro 2
10

Full ordering challenge

#

The hardest variant — predict all 8 lines of output: ```js console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => { console.log('3'); setTimeout(() => console.log('4'), 0); Promise.resolve().then(() => console.log('5')); }); setTimeout(() => console.log('6'), 0); Promise.resolve().then(() => console.log('7')); console.log('8'); ```

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
  console.log('3');
  setTimeout(() => console.log('4'), 0);
  Promise.resolve().then(() => console.log('5'));
});
setTimeout(() => console.log('6'), 0);
Promise.resolve().then(() => console.log('7'));
console.log('8');

// Output (8 lines): ???
Solution
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
  console.log('3');
  setTimeout(() => console.log('4'), 0);
  Promise.resolve().then(() => console.log('5'));
});
setTimeout(() => console.log('6'), 0);
Promise.resolve().then(() => console.log('7'));
console.log('8');

// Output: 1, 8, 3, 7, 5, 2, 6, 4
//
// Phase 1 sync:  1, queue macro(2), queue micro(first .then), queue macro(6), queue micro(7), 8
// Phase 2 micro: run first .then -> 3, queue macro(4), queue micro(5)
//                run micro(7) -> 7
//                run micro(5) -> 5
// Phase 3 macro: 2, 6, 4  (FIFO order)