Call Stack vs Task Queue vs Microtask Queue in JS

The call stack, task queue, and microtask queue decide what runs and when. Learn their differences and why Promise callbacks always beat setTimeout.

7 min read

JavaScript has three structures that decide the order code runs: the call stack, the task queue, and the microtask queue. Together they form the execution model that the event loop orchestrates. Understanding which queue a callback enters is the key to predicting your code's output.

The call stack executes synchronous code immediately. The two queues hold asynchronous callbacks until the stack is empty. The difference is priority: microtasks always run before the next task.

Quick Comparison

Call StackMicrotask QueueTask Queue (Macrotask)
What goes hereSynchronous function callsPromise handlers, queueMicrotask, MutationObserversetTimeout, setInterval, event listeners, fetch responses
When it runsImmediately, one frame at a timeAfter every task, before the next taskOne callback per event loop iteration
Can it add more workYes, by calling functionsYes, and new microtasks run in the same cycleYes, but new tasks wait for the next iteration
Emptying behaviorRuns until sync block finishesRuns until completely emptyOne task taken per iteration

The Event Loop Algorithm

The event loop follows a fixed mechanical sequence. It does not pick the most important thing. It follows a repeatable order.

Event loop: stack, microtask queue, task queue

The loop repeats: run synchronous code until the stack is empty, drain every microtask, then take exactly one task. Between tasks, the browser may also render if a frame is due.

Every piece of JavaScript runs on the call stack. The queues only hold work until the stack is ready.

Example: All Three in Action

javascriptjavascript
console.log("1: synchronous");
 
setTimeout(() => {
  console.log("2: task queue (setTimeout)");
}, 0);
 
Promise.resolve().then(() => {
  console.log("3: microtask queue (Promise)");
});
 
console.log("4: synchronous");

The output shows the execution order clearly. The synchronous calls always run first, then any microtasks drain before the next task gets a turn:

texttext
1: synchronous
4: synchronous
3: microtask queue (Promise)
2: task queue (setTimeout)

Here is what happens step by step. First, both synchronous console.log calls run on the stack, printing 1 and 4. The setTimeout registers its callback with the browser timer.

The Promise immediately queues its handler in the microtask queue. When the stack empties, the event loop drains the microtask queue, running the Promise handler and printing 3. Only then does it take the setTimeout callback from the task queue and print 2.

Even with a delay of 0 milliseconds, the setTimeout callback runs last. The delay only determines when the callback enters the task queue, not when it runs on the stack.

Microtasks Run to Completion

The event loop does not take one microtask at a time. It drains the entire microtask queue before moving on. This means microtasks can add more microtasks, and they all run before any task gets a turn.

javascriptjavascript
Promise.resolve().then(() => {
  console.log("Microtask 1");
 
  Promise.resolve().then(() => {
    console.log("Microtask 2 (nested)");
  });
});
 
Promise.resolve().then(() => {
  console.log("Microtask 3");
});
 
setTimeout(() => {
  console.log("Task from setTimeout");
}, 0);

Running this produces output that shows how microtasks take priority over tasks. Even when microtasks add new microtasks, all of them complete before any task runs:

texttext
Microtask 1
Microtask 3
Microtask 2 (nested)
Task from setTimeout

Microtask 2 was queued during microtask 1's execution but still ran before the setTimeout task. The event loop kept draining microtasks until the queue was empty.

This is why a microtask that recursively adds more microtasks can starve the task queue and block rendering. Avoid infinite microtask chains.

Which Queue Does Each API Use?

APIQueueNotes
setTimeout(fn, ms)TaskEnters queue after ms milliseconds
setInterval(fn, ms)TaskRe-enters queue every ms milliseconds
Promise.then(fn)MicrotaskEnters queue when the promise resolves
Promise.catch(fn)MicrotaskSame as .then for rejections
queueMicrotask(fn)MicrotaskExplicit microtask scheduling
MutationObserverMicrotaskFires after observed DOM changes
addEventListener("click", fn)TaskUser events use the task queue
requestAnimationFrame(fn)Render queueRuns before the next paint

For a deeper look at setTimeout timing, see /javascript/javascript-settimeout-behavior-complete-guide. For the full Promise lifecycle, read /javascript/javascript-promises-complete-beginner-guide.

Nested Async: A Complete Example

Here is code that uses every queue type to show the full execution order.

javascriptjavascript
console.log("A: sync");
 
setTimeout(() => console.log("B: task 1"), 0);
 
Promise.resolve()
  .then(() => {
    console.log("C: microtask 1");
    Promise.resolve().then(() => {
      console.log("D: microtask 2 (nested)");
    });
  })
  .then(() => console.log("E: microtask 3 (chained)"));
 
setTimeout(() => {
  console.log("F: task 2");
  Promise.resolve().then(() => {
    console.log("G: microtask inside task 2");
  });
}, 0);
 
console.log("H: sync");

The output demonstrates the full execution order across all three queue types, showing how synchronous code, microtasks, and tasks interleave:

texttext
A: sync
H: sync
C: microtask 1
E: microtask 3 (chained)
D: microtask 2 (nested)
B: task 1
F: task 2
G: microtask inside task 2

This example shows several rules. All synchronous code runs first (A, H). All microtasks drain before any task (C, E, D).

Chained .then callbacks (E) run in order, and nested microtasks created inside another microtask (D) also run in the same checkpoint. The first task runs (B), then the second task (F). Microtasks added inside a task (G) drain before the next task begins.

queueMicrotask: When to Use It

queueMicrotask lets you explicitly schedule a function as a microtask without creating a Promise.

javascriptjavascript
console.log("Before");
 
queueMicrotask(() => {
  console.log("Microtask");
});
 
console.log("After");

The output confirms the microtask runs after synchronous code finishes. It executes before the browser processes any pending tasks from the task queue:

texttext
Before
After
Microtask

Use queueMicrotask when you need a function to run after the current synchronous code finishes but before any pending tasks. Common use cases include batching state updates and ensuring cleanup code runs before the next task or render frame.

For most async needs, Promises and async/await are clearer. Read about async/await in /javascript/javascript-async-await-guide-complete-tutorial.

Common Mistake: setTimeout(fn, 0) Does Not Run Immediately

A setTimeout with a delay of 0 does not run the callback right away. The 0 means the timer fires as soon as possible, placing the callback into the task queue. But the callback still waits for the stack to empty and for all microtasks to drain.

If you block the stack with a long synchronous loop, the timer fires during the loop, but the callback cannot run until the synchronous code finishes and the stack is empty. The 0 delay is a minimum, not a guarantee.

The Three Queues in One Rule

The simplest way to remember the ordering: run everything on the call stack until empty, run every microtask until empty, take one task and run it, then repeat from the microtask step. This cycle is the event loop.

It runs thousands of times per second. Every click handler, every fetch response, and every animation frame flows through this same sequence.

Rune AI

Rune AI

Key Insights

  • The call stack runs synchronous code, one function at a time.
  • The microtask queue holds Promise callbacks and drains completely after every task.
  • The task queue holds setTimeout and event callbacks, running one per event loop iteration.
  • Execution order: sync code, then all microtasks, then one task, then all microtasks again.
  • Microtasks can add more microtasks, which all run before the next task begins.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between a task and a microtask?

A task (macrotask) is a callback from setTimeout, setInterval, I/O, or UI events. A microtask is from Promise.then, queueMicrotask, or MutationObserver. Microtasks run after synchronous code finishes but before the next task.

Does the microtask queue always run before the task queue?

Yes. After every synchronous task completes and the stack is empty, the event loop empties the entire microtask queue before picking the next task. New microtasks added during processing also run first.

Conclusion

The call stack runs code now. The microtask queue runs callbacks after the current task but before the next task. The task queue runs callbacks one at a time, with microtasks drained between each task. This ordering explains why Promise callbacks always beat setTimeout callbacks of the same delay.