By Rahul — Google Frontend Engineer
Why This Matters
If you do not understand microtasks and macrotasks, you cannot predict when your code runs. This leads to race conditions, UI freezes, and bugs that only appear under load.
The Event Loop — Simplified
The browser event loop follows this cycle:
- Pick ONE macrotask from the queue (or wait for one)
- Execute it completely
- Execute ALL microtasks in the microtask queue (including any new ones added during this step)
- Render (if needed — approximately every 16ms)
- Go to step 1
What Goes Where?
Macrotasks
setTimeout/setIntervalsetImmediate(Node.js only)- I/O operations
- UI rendering events
MessageChannelrequestAnimationFrame(technically before render, but after microtasks)
Microtasks
Promise.then/catch/finallyqueueMicrotask()MutationObserverasync/await(the code after await)
The Classic Interview Question
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
queueMicrotask(() => console.log('4'));
console.log('5');
// Output: 1, 5, 3, 4, 2
// Synchronous first (1, 5)
// Then ALL microtasks (3, 4)
// Then macrotask (2)The Dangerous Part: Microtask Queue Draining
// This will FREEZE the browser
function freezeForever() {
Promise.resolve().then(freezeForever);
}
freezeForever();
// Microtasks keep adding microtasks
// The queue NEVER empties
// The browser NEVER gets to renderCompare with setTimeout:
// This will NOT freeze — each call is a new macrotask
function safeRecursion() {
setTimeout(safeRecursion, 0);
// Browser renders between each call
}Real Production Example
// Updating DOM then reading layout
element.style.height = '100px';
// BAD: Reading layout in microtask — may not reflect the change
Promise.resolve().then(() => {
console.log(element.offsetHeight); // Might be old value
});
// GOOD: Use requestAnimationFrame for layout reads after changes
element.style.height = '100px';
requestAnimationFrame(() => {
console.log(element.offsetHeight); // After render, correct value
});async/await and Microtasks
async function foo() {
console.log('A');
await Promise.resolve();
console.log('B'); // This runs as a microtask
}
console.log('C');
foo();
console.log('D');
// Output: C, A, D, B
// 'B' is a microtask — runs after all sync codeBest Practices
- Use
queueMicrotask()when you need something to run after current code but before rendering - Use
setTimeout(fn, 0)when you need to yield to the browser for rendering - Never create infinite microtask loops
- Use
requestAnimationFramefor visual updates
Summary
Microtasks run after the current task but before rendering. Macrotasks run one at a time with rendering between them. Understanding this determines whether your UI stays responsive.