Microtasks vs macrotasks
Microtasks drain to empty between every macrotask; macrotasks are one-per-loop-tick. Promises are micro, setTimeout is macro.
Two queues. Different rules. Knowing which is which decides whether your code is responsive or frozen.
A macrotask is one unit of scheduled work: a timer callback, a DOM event handler, an I/O callback, a parsed HTML chunk. The event loop runs exactly one macrotask per iteration, then checks if there is more to do.
A microtask is a continuation of the current logical unit: a promise reaction, a queueMicrotask callback, a MutationObserver. After every macrotask, the engine drains the entire microtask queue before doing anything else. If a microtask queues more microtasks, those run too.
The order rule is fixed:
- Run one macrotask to completion.
- Drain all microtasks.
- Render if needed.
- Repeat.
setTimeout(() => console.log("macro"), 0);
Promise.resolve().then(() => console.log("micro"));
console.log("sync");
// sync, micro, macroThe synchronous code finishes the current macrotask. The promise microtask drains before the next macrotask (the timer) fires.
Sources
Microtasks:
.then,.catch,.finallycallbacksawaitcontinuationsqueueMicrotask(fn)MutationObservercallbacks
Macrotasks:
setTimeout,setIntervalsetImmediate(Node)- I/O callbacks (Node, file system, network)
- DOM events (click, scroll, message)
MessageChannel.postMessagerequestAnimationFrame(special, runs at render time)
Why the distinction matters
Microtasks let you "finish a thought." When a promise resolves, you usually want the .then to run before anything else. If a click handler waits for a fetch and then updates state, you want all the state updates from that fetch to happen before the next click is processed.
Macrotasks let the browser breathe. Rendering, input handling, and other tab activity get a chance between macrotasks. They do not get a chance between microtasks.
This is why a buggy promise chain can freeze the page:
function loop() {
Promise.resolve().then(loop);
}
loop(); // frozen foreverThe microtask queue never empties. The browser never gets to render or handle clicks. The same pattern with setTimeout(loop, 0) would not freeze, because the browser gets to render and handle input between each timer fire.
If you need to yield to the browser without the 4ms setTimeout floor, use MessageChannel for a real task or scheduler.yield() in modern Chrome.
Learn more
- ArticleJake Archibald: Tasks, microtasks, queues and schedulesJake Archibald
- Docs
- Docs