One thread, one call stack — and a loop that, whenever the stack is empty, drains all microtasks and then takes the next macrotask. Predicting output order is a top interview skill.
The moving parts
One turn of the loop
Run a task
Take the oldest macrotask (the initial script is the first) and run it to completion.
Drain microtasks
Run every microtask — including ones queued while draining — until the queue is empty.
Render (browsers)
If it's time for a frame:
requestAnimationFramecallbacks, style, layout, paint.Repeat
Pick the next macrotask.
Predict the output
console.log("1: sync start");
setTimeout(() => console.log("6: timeout (macrotask)"), 0);
Promise.resolve()
.then(() => console.log("3: promise then (microtask)"))
.then(() => console.log("5: chained then (microtask)"));
queueMicrotask(() => console.log("4: queueMicrotask"));
console.log("2: sync end");1: sync start
2: sync end
3: promise then (microtask)
4: queueMicrotask
5: chained then (microtask)
6: timeout (macrotask)async/await in the ordering
async function task() {
console.log("B: task starts (sync part)");
await null; // everything after this is a microtask
console.log("E: task resumes after await");
}
console.log("A: script start");
setTimeout(() => console.log("G: timeout"), 0);
task();
new Promise((resolve) => {
console.log("C: executor runs synchronously");
resolve();
}).then(() => console.log("F: then"));
console.log("D: script end");A: script start
B: task starts (sync part)
C: executor runs synchronously
D: script end
E: task resumes after await
F: then
G: timeoutMicrotasks can starve the loop
let count = 0;
setTimeout(() => console.log("timeout finally runs, microtasks ran:", count), 0);
function spin() {
if (++count < 1000) queueMicrotask(spin); // keeps refilling the queue
}
queueMicrotask(spin);timeout finally runs, microtasks ran: 1000Node specifics: nextTick, setImmediate
| Node phase / queue | Runs |
|---|---|
process.nextTick queue | After the current operation, before promise microtasks (from CommonJS/callbacks) |
| Promise microtasks | Right after the nextTick queue |
| timers | Expired setTimeout / setInterval |
| poll | I/O callbacks (fs, network) |
| check | setImmediate |
| close | 'close' events |
setTimeout(() => {
console.log("inside a timer callback:");
setTimeout(() => console.log(" 4. setTimeout 0 (next timers phase)"), 0);
setImmediate(() => console.log(" 3. setImmediate (check phase comes first)"));
Promise.resolve().then(() => console.log(" 2. promise microtask"));
process.nextTick(() => console.log(" 1. process.nextTick"));
}, 0);inside a timer callback:
1. process.nextTick
2. promise microtask
3. setImmediate (check phase comes first)
4. setTimeout 0 (next timers phase)What is the difference between microtasks and macrotasks?
If JavaScript is single-threaded, how does it do async work?