ESC

Type to search the knowledge base.

queueMicrotask vs setTimeout

Microtask vs macrotask scheduling — queueMicrotask, Promise.then, setTimeout(0), and why microtasks can starve rendering.

intermediate3 min read
  • javascript
  • queuemicrotask-vs

Both defer work until “later.” They use different queues, so order and UI impact differ. This is event-loop detail interviewers probe after the classic promise-vs-timeout question.

The rule

After a macrotask finishes (script, timer callback, many I/O events):

  1. Drain all microtasks (including ones queued while draining).
  2. Maybe render.
  3. Next macrotask (setTimeout, etc.).
console.log('A');

setTimeout(() => console.log('D timeout'), 0);

queueMicrotask(() => console.log('C microtask'));

Promise.resolve().then(() => console.log('C2 promise'));

console.log('B');

// A, B, C microtask, C2 promise, D timeout

queueMicrotask(fn) and Promise.resolve().then(fn) are the same queue priority. Prefer queueMicrotask when you don’t need promise semantics.

setTimeout(0) is not “next line”

It’s “soon, as a macrotask,” after microtasks, often with a minimum delay clamp under nested timers. Use it to yield to the browser (paint, input) between chunks of work.

async function chunked(items, worker) {
  for (let i = 0; i < items.length; i++) {
    worker(items[i]);
    if (i % 100 === 0) {
      await new Promise((r) => setTimeout(r, 0)); // yield
    }
  }
}

Yielding only with queueMicrotask does not allow rendering — microtasks run before paint.

When to use which

Need Prefer
Run after current JS, before paint/timers queueMicrotask / promise then
Let browser paint / handle events setTimeout, MessageChannel, scheduler.yield
MutationObserver-style batching already microtask don’t double-schedule blindly
// Flatten async completion to microtask without a Promise library
function defer(fn) {
  queueMicrotask(fn);
}

Starvation

function bomb() {
  queueMicrotask(bomb);
}
bomb(); // page freezes — macrotasks never run

Microtask storms are real. Keep chains finite.

Node note

Node has additional phases (setImmediate, process.nextTick). process.nextTick is even more aggressive than microtasks in Node’s model. In browsers, stick to microtask vs timer story.

Interview answer (out loud)

“queueMicrotask schedules a microtask; setTimeout schedules a macrotask. After each task the engine drains microtasks before the next timer or render. That’s why microtasks run before setTimeout(0). I use microtasks for ‘after this sync code’ and timers/message channel when I need to yield for paint.”

Promise then vs queueMicrotask

queueMicrotask(() => console.log(1));
Promise.resolve().then(() => console.log(2));
queueMicrotask(() => console.log(3));
// 1, 2, 3 — FIFO microtask queue order

.then always creates a Promise reaction; queueMicrotask is lighter when you only need scheduling. Libraries that must run after DOM mutations often use microtasks (Vue’s nextTick history, Angular zones — different stacks, same idea).

MessageChannel as a macrotask yield

function postTask(fn) {
  const { port1, port2 } = new MessageChannel();
  port1.onmessage = () => fn();
  port2.postMessage(null);
}

Faster than nested setTimeout(0) clamps in some browsers; still a task boundary that allows paint — unlike microtasks.

Debugging order bugs

When “this ran before paint but after my promise,” log with labels and remember: sync → microtasks → (render opportunity) → next task. Mixing await (microtask resume) with timers is the usual story.

Further reading

Related guides