Microtasks vs Macrotasks in JavaScript (Event Loop Priority)

HighHardJavascript
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

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.

Answer

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

Promise.then/catch/finally, queueMicrotask, MutationObserver

After the current call stack, before the next macrotask and typically before rendering

Higher priority: can “cut in line” before timers/events

Macrotask queue

setTimeout, setInterval, DOM events, message events, I/O callbacks

Next event loop turn; one macrotask runs, then microtasks drain again

Lets the browser breathe: rendering/input can happen between turns

Microtasks are higher priority than macrotasks.

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

JAVASCRIPT
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.

JAVASCRIPT
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)
JAVASCRIPT
// 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.

Similar questions
Guides
10 / 61