JavaScript · Syntax · Advanced
The event loop
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`.
Quick topic start and explanations before exercises (exercises below):
Ordering examples, async/await, microtask-in-macrotask, output prediction
#Quick reference table, pitfalls, await-in-loop, Web Workers
#Exercises:
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
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'.
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.
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
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.
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.
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'.
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.
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
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)