JavaScriptMicrotasks & Macrotasks

Microtasks & Macrotasks

The event loop juggles two queues: macrotasks (also called tasks) and microtasks. Knowing which API queues into which — and the order they drain in — is the key to predicting when a callback actually runs. It is also the difference between code that feels snappy and code that drops frames.

The two queues at a glance

Each turn of the event loop picks one macrotask, runs it, then drains every microtask before doing anything else. Microtasks are higher priority and run sooner.

  • Macrotasks — one per loop turn. The next macrotask only starts after all microtasks have drained.

  • Microtasks — drained completely between macrotasks. New microtasks queued during the drain run in the same drain.

Which API uses which queue?
  • MicrotaskPromise.then, Promise.catch, Promise.finally, the resumption after await, queueMicrotask, MutationObserver callbacks.

  • MacrotasksetTimeout, setInterval, I/O callbacks, UI events (click, keydown), postMessage, MessageChannel, setImmediate in Node.

  • Animation framerequestAnimationFrame callbacks run between macrotasks and the next paint, separate from both queues.

Why this split?
Microtasks let promise chains run *as a single conceptual step* without something else slipping in between. Macrotasks are the "yield to the browser" boundary — they let the engine paint, handle input, and run animation frames.
Promise vs setTimeout

JS
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("sync");
sync
promise
timeout

Even though both are queued with no delay, the promise wins. The synchronous code runs first, then microtasks drain (promise), then the next macrotask runs (timeout).

setTimeout vs setInterval

Both are macrotask APIs. setTimeout(fn, ms) queues one task after at least ms milliseconds. setInterval(fn, ms) queues a fresh task every ms milliseconds, regardless of whether previous callbacks finished.

JS
// setInterval can pile up if the callback is slow.
const id = setInterval(() => {
  console.log("tick", Date.now());
}, 100);

// Stop it later
setTimeout(() => clearInterval(id), 500);
setInterval is rarely what you want
If the work inside the interval takes longer than the interval itself, callbacks queue up and fire back-to-back. A recursive `setTimeout` gives you proper spacing between runs.

Recursive setTimeout — self-spacing

JS
function tick() {
  console.log("tick");
  setTimeout(tick, 100);   // schedule next only after this one finishes
}
tick();
requestAnimationFrame

requestAnimationFrame(cb) is for visual updates. The browser runs the callback once per frame, right before the next paint — usually about every 16 ms on a 60 Hz display. It is not a microtask and not a regular macrotask; it lives between them and the render step.

JS
function animate(time) {
  // 'time' is a high-resolution timestamp in ms
  el.style.transform = "translateX(" + (time / 10) + "px)";
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
  • Drops to ~30 fps automatically when the device is busy — safer than setInterval(animate, 16).

  • Pauses entirely in background tabs, saving battery.

  • Aligned with the paint, so visual changes look smooth.

queueMicrotask

When you need a microtask without the overhead of creating a promise, use queueMicrotask. It schedules a function to run after the current task but before any other macrotask.

JS
console.log("a");

queueMicrotask(() => console.log("b"));
Promise.resolve().then(() => console.log("c"));
setTimeout(() => console.log("d"), 0);

console.log("e");
a
e
b
c
d
Microtasks drain fully — sometimes too fully

If a microtask queues another microtask, the new one runs in the same drain. This is great for chained promises, but it can starve rendering if a chain never ends.

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

setTimeout(() => console.log("timeout"), 0);
then 1
then 2
then 3
timeout

All three then callbacks run before the timeout because they keep adding to the microtask queue while the drain is in progress.

Cheat-sheet: priority order
    1. Synchronous code currently on the call stack.
    1. Microtasks — drained fully after every macrotask.
    1. requestAnimationFrame callbacks — once per frame, before paint.
    1. Browser render (style, layout, paint).
    1. The next macrotasksetTimeout, I/O, UI events…
When to use what
For "do this after the current event handler" use `queueMicrotask` or a resolved promise. For "do this after the browser repaints" use `requestAnimationFrame`. For "do this in 250 ms" use `setTimeout`. For polling, prefer `setTimeout` over `setInterval`.