Microtasks & Macrotasks

Two queues, different priorities. Why Promises always beat setTimeout.

Two Queues

Not all async callbacks are equal.

Microtask Queue

  • • Promise .then/.catch/.finally
  • • async/await continuations
  • • queueMicrotask()
  • • MutationObserver

HIGH PRIORITY — drain ALL

Macrotask Queue

  • • setTimeout / setInterval
  • • DOM events (click, etc.)
  • • fetch response callbacks
  • • requestAnimationFrame*

LOWER PRIORITY — one at a time

// Quick proof — microtasks beat macrotasks:
setTimeout(() => console.log("macro"), 0);
Promise.resolve().then(() => console.log("micro"));
console.log("sync");

// Output: sync, micro, macro
// Even though both have 0 delay,
// microtask runs first because it has priority

Microtasks

High-priority tasks that drain completely before anything else.

// Promise callbacks are microtasks:
Promise.resolve()
  .then(() => console.log("micro 1"))
  .then(() => console.log("micro 2"))
  .then(() => console.log("micro 3"));

// async/await — code after await is a microtask:
async function example() {
  console.log("before await");    // sync
  await fetch("/api");            // pauses here
  console.log("after await");     // microtask!
}

// queueMicrotask — explicitly schedule a microtask:
queueMicrotask(() => {
  console.log("explicit microtask");
});

// MutationObserver — DOM mutation callbacks:
const observer = new MutationObserver(() => {
  console.log("DOM changed"); // microtask
});
// Key rule: ALL microtasks drain before moving on
// Even microtasks created during microtask processing!

Promise.resolve().then(() => {
  console.log("micro 1");
  // This creates a NEW microtask — it runs BEFORE any macrotask:
  Promise.resolve().then(() => console.log("micro 2"));
});

setTimeout(() => console.log("macro"), 0);

// Output: micro 1, micro 2, macro
// micro 2 was created inside micro 1, but still runs
// before the macrotask — the queue is fully drained

Macrotasks

Regular tasks — one per event loop iteration.

// setTimeout/setInterval — classic macrotasks:
setTimeout(() => console.log("task 1"), 0);
setTimeout(() => console.log("task 2"), 0);
setTimeout(() => console.log("task 3"), 0);

// Each runs in a separate event loop iteration:
// Iteration 1: task 1 → drain microtasks → render
// Iteration 2: task 2 → drain microtasks → render
// Iteration 3: task 3 → drain microtasks → render

// DOM events are also macrotasks:
button.addEventListener("click", () => {
  console.log("click handler"); // macrotask
  Promise.resolve().then(() => console.log("micro in click"));
});
// When clicked: "click handler", "micro in click"
// The microtask runs before the next macrotask/render

// I/O callbacks (fetch completion, file read):
fetch("/api").then(r => r.json()).then(data => {
  // The .then is a microtask, but the fetch completion
  // that triggers it originates as a macrotask
});

Priority Rules

The complete execution order.

// The event loop cycle:
// 1. Execute synchronous code (call stack)
// 2. Drain ALL microtasks
// 3. ONE macrotask
// 4. Drain ALL microtasks (again)
// 5. Render (if needed)
// 6. Repeat from step 3

console.log("1: sync");

setTimeout(() => {
  console.log("4: macro 1");
  Promise.resolve().then(() => console.log("5: micro inside macro"));
}, 0);

setTimeout(() => {
  console.log("6: macro 2");
}, 0);

Promise.resolve().then(() => console.log("2: micro 1"));
Promise.resolve().then(() => console.log("3: micro 2"));

// Output: 1, 2, 3, 4, 5, 6

Sync runs first, then ALL microtasks, then ONE macrotask, then ALL microtasks created during that macrotask, then next macrotask.

1 / 2

Starvation

Microtasks can block everything else.

// ⚠️ DANGER: Microtasks that create more microtasks
// can starve macrotasks and rendering:

function infiniteMicrotasks() {
  Promise.resolve().then(() => {
    console.log("micro");
    infiniteMicrotasks(); // creates another microtask
  });
}
infiniteMicrotasks();
// The microtask queue NEVER empties!
// setTimeout callbacks never run
// The page never renders
// Same effect as an infinite while loop

// Safe pattern — break work into macrotasks:
function processItems(items) {
  if (items.length === 0) return;
  const item = items.shift();
  process(item);
  setTimeout(() => processItems(items), 0); // yields to render
}

// Or use requestIdleCallback for low-priority work:
requestIdleCallback((deadline) => {
  while (deadline.timeRemaining() > 0 && tasks.length) {
    tasks.shift()();
  }
});

Rule of thumb:

Microtasks for quick continuations (promise chains). Macrotasks (setTimeout) when you need to yield to the browser for rendering or user input.

FAQ

Common questions about task scheduling.