· tips  · 5 min read

Debunking Myths: Understanding Microtasks and Macrotasks in Async JavaScript

Clear up misconceptions about microtasks and macrotasks. Learn how the JavaScript event loop actually schedules Promise callbacks, setTimeout, MutationObserver, and Node's process.nextTick - with concrete examples and practical rules you can use today.

Clear up misconceptions about microtasks and macrotasks. Learn how the JavaScript event loop actually schedules Promise callbacks, setTimeout, MutationObserver, and Node's process.nextTick - with concrete examples and practical rules you can use today.

Outcome first: by the end of this article you’ll be able to predict - every time - whether a Promise callback will run before a setTimeout callback, why heavy microtasks block rendering, and how Node’s process.nextTick fits into the picture. You’ll also gain simple, actionable rules to avoid subtle async bugs.

The hook: one surprising fact

Promises resolve in a microtask queue that runs before the next task (macrotask) - so a Promise.then callback almost always runs before a setTimeout 0 callback. Short. Surprising. Crucial.

What people commonly get wrong

  • Myth: “setTimeout(…, 0) runs immediately after current code.” No - it’s scheduled as a macrotask and runs only after microtasks and rendering (and after other macrotasks already queued).
  • Myth: “Promise callbacks are ‘background’ or lower priority.” Wrong - Promise callbacks (microtasks) run before the next macrotask and before rendering.
  • Myth: “All JS runtimes do the same thing.” Not exactly - browsers and Node share the event loop idea but differ in details (e.g., Node’s process.nextTick behavior).

These misunderstandings cause race conditions, UI jank, and puzzling output ordering in logs. Let’s demystify the real mechanics.

A compact view of the event loop

At runtime, JavaScript engines coordinate several queues and phases. Simplified (browser-focused):

  1. Run code on the call stack (current task).
  2. When the call stack is empty, run all microtasks (job queue) to completion.
  3. Optionally update rendering (paint) if needed.
  4. Take the next macrotask (task) from the task queue and run it. Repeat.

Microtasks include Promise reactions and queueMicrotask and MutationObserver callbacks. Macrotasks include setTimeout/setInterval callbacks, I/O, UI events, and others.

References: the authoritative MDN explainer on the event loop and Jake Archibald’s deep dive are excellent reads:

Microtasks vs macrotasks - concrete examples

Let’s look at typical browser examples and their outputs to cement intuition.

Example A - Promise vs setTimeout

console.log('start');

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

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

console.log('end');

Expected output:

start
end
promise
timeout

Why: the synchronous code runs first (‘start’, ‘end’). When the call stack empties, the engine drains the microtask queue (the Promise.then), so ‘promise’ runs before the macrotask scheduled by setTimeout.

Example B - nested microtasks

Promise.resolve().then(() => {
  console.log('p1');
  Promise.resolve().then(() => console.log('p2'));
});

console.log('sync');

Output:

sync
p1
p2

Why: microtasks scheduled while draining microtasks are enqueued and will run in the same microtask-turn before returning to macrotasks.

Example C - queueMicrotask (explicit microtask)

console.log('start');

queueMicrotask(() => console.log('qm'));

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

setTimeout(() => console.log('timeout'));

console.log('end');

Output order: start, end, qm, p, timeout - note microtasks run before macrotasks; queueMicrotask and Promise callbacks are both microtasks, but their relative ordering follows scheduling order.

Node specifics: process.nextTick vs Promise microtasks

Node has an extra queue: process.nextTick callbacks. They run before other microtasks (including Promise reactions). This means process.nextTick can starve the event loop if overused.

Example (Node):

console.log('start');

process.nextTick(() => console.log('nextTick'));

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

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

console.log('end');

Typical Node output:

start
end
nextTick
promise
timeout

Note: process.nextTick runs before Promise microtasks in Node. See Node docs: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

Visualizing priorities (simple mental model)

  • Call stack (synchronous code) - runs now.
  • Microtask queue (Promise.then, queueMicrotask, MutationObserver) - drained after the current task finishes and before rendering / next macrotask.
  • Macrotask queue (setTimeout, setInterval, I/O callbacks, UI events) - one macrotask runs, then microtasks are drained, then paint, then next macrotask.
  • Node addition: process.nextTick sits even above the Promise microtask queue.

Common pitfalls and how to avoid them

  1. UI jank from heavy microtasks

    • Problem: running many or long microtasks blocks rendering because microtasks run before paint.
    • Fix: chunk heavy work into macrotasks (setTimeout, requestAnimationFrame) or use Web Workers.
  2. Relying on ordering between different macrotask sources

    • Problem: different browsers or environments may schedule macrotasks in different orders (timers vs I/O), so ordering isn’t guaranteed.
    • Fix: use explicit chaining (Promises) or coordination (async/await) for predictable ordering.
  3. Starving the loop with process.nextTick (Node)

    • Problem: unbounded nextTick scheduling prevents I/O and timers from running.
    • Fix: avoid scheduling大量 nextTick in production paths; use setImmediate or regular callbacks for longer sequences.
  4. Assuming setTimeout(fn, 0) is “instant”

    • Problem: it waits until the next macrotask and after all microtasks and paints.
    • Fix: use microtasks if you need something to run immediately after the current synchronous work (Promise.resolve().then or queueMicrotask).

Practical recipes

  • Need to run immediately after current synchronous code but before rendering? Use queueMicrotask() or Promise.resolve().then(…).
  • Need to wait until after painting? Use requestAnimationFrame for the next frame, or setTimeout(fn, 0) but rAF is tied to paint timing.
  • Need to break up heavy work? Use setTimeout 0 or requestIdleCallback (where supported) or Web Workers.

Short code cheatsheet

  • Run next microtask: Promise.resolve().then(fn) or queueMicrotask(fn)
  • Run next macrotask: setTimeout(fn, 0) or setImmediate(fn) (Node)
  • Node higher-priority microtask: process.nextTick(fn)

When order matters (practical scenarios)

  • Animation sequencing: schedule DOM reads/writes carefully - don’t hide writes in microtasks that could block a paint.
  • Deterministic test assertions: prefer Promise-based chaining (await) to guarantee ordering rather than setTimeout-based waits.
  • Library authors: don’t assume setTimeout occurs before other macrotasks; instead expose explicit hooks or return Promises.

Summary: the single strongest takeaway

Microtasks run after the current call stack finishes but before the next macrotask and before the browser paints - so Promise callbacks are higher-priority than setTimeout callbacks. Learn to think in terms of “call stack → microtasks → paint → macrotask” (and remember Node’s extra nextTick queue) - and you’ll stop being surprised by order-of-execution bugs.

Further reading

Back to Blog

Related Posts

View All Posts »
The Mystery of Microtasks vs. Macrotasks: A Deep Dive

The Mystery of Microtasks vs. Macrotasks: A Deep Dive

Understand how microtasks and macrotasks differ in the JavaScript event loop, see compact examples that expose surprising ordering and starvation issues, and learn practical patterns to use each correctly in browsers and Node.js.

Understanding the Event Loop: Myths vs. Reality

Understanding the Event Loop: Myths vs. Reality

Cut through the noise: learn what the JavaScript event loop actually does, why common claims (like “setTimeout(0) runs before promises”) are wrong, how browsers and Node differ, and how to reason reliably about async code.

Microtasks vs Macrotasks: The Great JavaScript Showdown

Microtasks vs Macrotasks: The Great JavaScript Showdown

A deep dive into microtasks and macrotasks in JavaScript: what they are, how the event loop treats them, how they affect rendering and UX, illustrative diagrams and real-world examples, plus practical guidance to avoid performance pitfalls.

The Dangers of eval: A Cautionary Tale

The Dangers of eval: A Cautionary Tale

A deep dive into why eval and its cousins (new Function, setTimeout(string)) are dangerous, illustrated with real-world-style examples and concrete mitigations for web and Node.js applications.