In each event loop turn, JavaScript runs synchronous code, then drains the entire microtask queue (e.g., Promise.then, queueMicrotask) before taking the next macrotask (e.g., setTimeout, DOM events). This priority explains why Promises run before timers and why long microtask chains can delay rendering.
Microtasks vs Macrotasks in JavaScript (Event Loop Priority)
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core difference
Both are “queued callbacks” that run after the current synchronous call stack finishes. The difference is priority and timing.
- Microtasks run immediately after the call stack and are fully drained before anything else happens.
- Macrotasks run one at a time on the next event loop turn, and the browser may render between macrotasks.
Queue | Common sources | When it runs | Why it matters |
|---|---|---|---|
Microtask queue |
| After the current call stack, before the next macrotask and typically before rendering | Higher priority: can “cut in line” before timers/events |
Macrotask queue |
| Next event loop turn; one macrotask runs, then microtasks drain again | Lets the browser breathe: rendering/input can happen between turns |
Event loop rule of thumb (browser)
For each turn:
1) Run all synchronous code (call stack)
2) Drain microtasks completely
3) Run one macrotask
4) Render (if needed) / handle UI
5) Repeat
console.log('A');
setTimeout(() => console.log('B (macrotask: timer)'), 0);
Promise.resolve()
.then(() => console.log('C (microtask: promise)'))
.then(() => console.log('D (microtask: promise)'));
console.log('E');
// Output:
// A
// E
// C
// D
// B
Interview punchline: “Promises run before timers because microtasks are drained before the next macrotask.”
Common pitfall: microtasks can starve rendering
Because the microtask queue is drained completely, a long/recursive microtask chain can delay painting and make the UI feel frozen.
function starveUI() {
queueMicrotask(starveUI); // or Promise.resolve().then(starveUI)
}
starveUI();
// Result: browser can't yield to rendering/input.
Practical guidance
- Use microtasks for “run right after this finishes” follow-ups (e.g., finalize state, chain promise work).
- Use macrotasks to yield control (e.g., split heavy work so the UI can render):
setTimeout(fn, 0)(coarse)requestAnimationFrame(before next paint)requestIdleCallback(when idle; best-effort)
// Yield so the UI can render between chunks
function doChunkedWork(items, chunkSize = 200) {
let i = 0;
function runChunk() {
const end = Math.min(i + chunkSize, items.length);
for (; i < end; i++) {
// heavy work
}
if (i < items.length) setTimeout(runChunk, 0); // macrotask yield
}
runChunk();
}
Node.js note (don’t overclaim)
Node also has microtasks (Promises) and macrotask-like phases (timers, I/O, check, etc.). Microtasks are processed between phases, and process.nextTick has even higher priority than Promise microtasks. In interviews: keep the core browser model correct, then mention Node differences briefly if asked.
One-sentence answer
Microtasks (Promises/queueMicrotask) run immediately after the current stack and are drained fully before moving on, while macrotasks (timers/events) run one per turn, allowing the runtime/browser to render between turns.