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.
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 Stack | Microtask Queue | Task Queue (Macrotask) | |
|---|---|---|---|
| What goes here | Synchronous function calls | Promise handlers, queueMicrotask, MutationObserver | setTimeout, setInterval, event listeners, fetch responses |
| When it runs | Immediately, one frame at a time | After every task, before the next task | One callback per event loop iteration |
| Can it add more work | Yes, by calling functions | Yes, and new microtasks run in the same cycle | Yes, but new tasks wait for the next iteration |
| Emptying behavior | Runs until sync block finishes | Runs until completely empty | One 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.
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
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:
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.
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:
Microtask 1
Microtask 3
Microtask 2 (nested)
Task from setTimeoutMicrotask 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?
| API | Queue | Notes |
|---|---|---|
| setTimeout(fn, ms) | Task | Enters queue after ms milliseconds |
| setInterval(fn, ms) | Task | Re-enters queue every ms milliseconds |
| Promise.then(fn) | Microtask | Enters queue when the promise resolves |
| Promise.catch(fn) | Microtask | Same as .then for rejections |
| queueMicrotask(fn) | Microtask | Explicit microtask scheduling |
| MutationObserver | Microtask | Fires after observed DOM changes |
| addEventListener("click", fn) | Task | User events use the task queue |
| requestAnimationFrame(fn) | Render queue | Runs 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.
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:
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 2This 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.
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:
Before
After
MicrotaskUse 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
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.
Frequently Asked Questions
What is the difference between a task and a microtask?
Does the microtask queue always run before the task queue?
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.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.