JS Microtasks vs Macrotasks: A Complete Guide

Microtasks and macrotasks are the two queue types that determine async callback order in JavaScript. Learn their differences and why Promise callbacks run before setTimeout.

7 min read

Microtasks vs macrotasks describes the two asynchronous queues in JavaScript: the microtask queue and the macrotask queue (also called the task queue). Every async callback enters one of these two queues. Which queue it enters determines when it runs relative to other callbacks.

Microtasks have higher priority. The event loop drains the entire microtask queue before taking a single macrotask. This is why a Promise.then callback always runs before a setTimeout callback registered at the same time.

Quick Comparison

MicrotaskMacrotask (Task)
SourcesPromise.then/catch/finally, queueMicrotask, MutationObserversetTimeout, setInterval, event listeners, fetch, I/O
ProcessingEntire queue drained at onceOne callback per iteration
Can add moreYes, and they run in the same cycleYes, but they wait for the next iteration
Blocks renderingYes, if microtasks keep adding moreOne long task can block
Use caseWork that must finish before the next renderDeferred work, yielding to the event loop

Where Each Queue Fits in the Event Loop

The event loop follows a fixed sequence. Understanding where microtasks and macrotasks fit in that sequence is essential.

Event loop iteration with microtask and macrotask queues

Each iteration takes one macrotask, runs it to completion, then drains every microtask including any new ones added during draining. Only after the microtask queue is empty does the next macrotask begin. Between macrotasks, the browser may also render.

Visual Example: Execution Order

Here is code that mixes microtasks and macrotasks to show the order clearly.

javascriptjavascript
console.log("1: sync");
 
setTimeout(() => console.log("2: macrotask"), 0);
 
Promise.resolve().then(() => console.log("3: microtask"));
 
console.log("4: sync");

The output is predictable: 1, 4, 3, 2. Both synchronous calls run first because they are on the call stack. The Promise handler enters the microtask queue and runs before the setTimeout callback in the macrotask queue.

The delay argument to setTimeout does not change priority. Even with a delay of zero milliseconds, the macrotask queue is always processed after the microtask queue.

How Microtasks Can Starve Macrotasks

Because the microtask queue drains completely, a microtask that recursively adds more microtasks can block macrotasks and rendering indefinitely.

javascriptjavascript
function recursiveMicrotask(count) {
  if (count <= 0) return;
  console.log("Microtask", count);
  Promise.resolve().then(() => recursiveMicrotask(count - 1));
}
 
recursiveMicrotask(1000);
 
setTimeout(() => console.log("This never prints first"), 0);

All one thousand microtask callbacks run before the setTimeout callback. The browser cannot render or process user input until the microtask chain finishes. This is why queueMicrotask and recursive Promise chains should be used carefully.

For more on event loop architecture, see /javascript/the-js-event-loop-architecture-complete-guide.

Which APIs Feed Which Queue

Knowing the source of each callback type helps you predict execution order.

APIQueue TypeWhen Callback Enters Queue
Promise.then/catch/finallyMicrotaskImmediately when the promise settles
queueMicrotask(fn)MicrotaskImmediately
MutationObserverMicrotaskAfter observed DOM mutations
setTimeout(fn, ms)MacrotaskAfter ms milliseconds elapse
setInterval(fn, ms)MacrotaskEvery ms milliseconds
addEventListener("click", fn)MacrotaskWhen the user clicks
fetch().then(fn)Macrotask (fetch), Microtask (.then)fetch completes, then .then is queued
requestAnimationFrame(fn)Render queueBefore the next paint
process.nextTick(fn)Node.js nextTickHighest priority in Node.js

The fetch API is interesting because it spans both queues. The network request itself is handled as a macrotask. When the response arrives, the Promise resolves and its .then handler enters the microtask queue.

Macrotasks for Yielding to the Event Loop

One practical use of macrotasks is yielding control back to the event loop. A long synchronous operation can be broken into chunks using setTimeout with a delay of zero.

javascriptjavascript
function processLargeArray(items, chunkSize) {
  let index = 0;
 
  function nextChunk() {
    const end = Math.min(index + chunkSize, items.length);
    for (let i = index; i < end; i += 1) {
      items[i] = items[i] * 2;
    }
    index = end;
 
    if (index < items.length) {
      setTimeout(nextChunk, 0);
    }
  }
 
  nextChunk();
}

Each chunk runs as a separate macrotask. Between chunks, the event loop can process microtasks, handle user input, and render updates. Without this pattern, the entire operation would block the page until complete.

Microtasks for Batching Updates

Microtasks are useful when you need work to complete before the browser renders. Framework state updates often use microtasks for this reason.

javascriptjavascript
let state = { count: 0 };
 
function updateState(changes) {
  Object.assign(state, changes);
  queueMicrotask(() => {
    console.log("State updated:", state.count);
  });
}
 
updateState({ count: 1 });
updateState({ count: 2 });
updateState({ count: 3 });

All three updateState calls run synchronously, mutating the state object. A single microtask then logs the final state. Only one microtask runs, not three, because each queueMicrotask call is separate but they all run before any macrotask.

Common Mistake: Confusing Timing with Priority

A setTimeout callback with a delay of 100ms does not have lower priority than one with 0ms. Both are macrotasks with equal priority. The delay only determines when each callback enters the queue, not its priority within the queue.

Once a macrotask enters the queue, it waits for the current task to finish and for all microtasks to drain. Two setTimeout callbacks with delays of 0ms and 100ms both wait equally for the microtask queue to empty. The 0ms callback enters the queue sooner, but both have the same priority once they are in the queue.

For timing details on setTimeout specifically, see /javascript/javascript-settimeout-behavior-complete-guide.

Rune AI

Rune AI

Key Insights

  • Microtasks come from Promises and queueMicrotask. They drain completely after every macrotask.
  • Macrotasks come from setTimeout, event listeners, and I/O. One runs per event loop iteration.
  • Microtasks always run before the next macrotask, regardless of timing.
  • Microtasks can add more microtasks, which all run in the same cycle.
  • Choose microtasks for work that must complete before the next render frame.
RunePowered by Rune AI

Frequently Asked Questions

Which is faster: microtask or macrotask?

Neither is faster in terms of execution speed. The difference is scheduling priority. Microtasks always run before the next macrotask, so a microtask callback executes sooner than a macrotask callback registered at the same time.

Can I convert a macrotask into a microtask?

Yes. Wrap the macrotask callback inside a Promise or use queueMicrotask. For example, instead of setTimeout(fn, 0), use Promise.resolve().then(fn) or queueMicrotask(fn) to schedule the callback as a microtask.

Conclusion

Microtasks and macrotasks are the two lanes of the event loop. Microtasks have higher priority and drain completely between macrotasks. Macrotasks run one at a time per event loop iteration. Knowing which APIs feed into which queue is the key to predicting async execution order.