Asynchronous JavaScript
Microtasks vs Macrotasks
Master task scheduling priorities in JavaScript. Learn the execution precedence rules between the Microtask Queue and the Macrotask Queue.
1. Introduction
Asynchronous callbacks are scheduled in two separate queues: the Microtask Queue and the Macrotask Queue (commonly called the Callback Queue). The JavaScript engine processes these queues with different priorities, which affects the order in which code executes.
2. Why It Matters
Understanding queue priorities is critical to predicting the execution order of complex asynchronous code (mixing Promises, timers, and mutation observers) and preventing UI rendering blocks.
3. Real-World Analogy
Think of a Post Office Service counter:
- Macrotasks (Standard Mail Delivery): Packages arriving at the loading dock. Each package delivery is a separate, standalone task. The clerk processes one package, then takes a break to check on other duties before processing the next.
- Microtasks (VIP Customers at the Counter): A customer standing directly at the window who keeps adding requests: "Oh, and mail this document, and photocopy this, and stamps please." The clerk must fulfill all of this customer's immediate requests (exhaust the microtask queue) before leaving the counter to process the packages at the loading dock (macrotasks) or clean the lobby (DOM render).
4. The Execution Rules
The Event Loop coordinates the execution of tasks using these priority rules:
1. Execute all synchronous code on the Call Stack first.
2. When the Call Stack is empty, check the Microtask Queue. Execute all microtasks in the queue sequentially until it is completely empty. If a microtask adds another microtask, it is executed immediately in the same cycle.
3. When the Microtask Queue is empty, check if the browser needs to perform a DOM rendering update.
4. Fetch the first task from the Macrotask Queue, push it onto the Call Stack, and execute it.
5. Repeat the loop (go back to Step 2).
5. Task Types
| Queue Type | Task Sources | Priority & Execution Behavior |
|---|---|---|
| Microtasks | Promise.then/catch/finally, queueMicrotask(), MutationObserver, process.nextTick (Node) |
High priority. The queue is executed until completely empty before moving on. |
| Macrotasks | setTimeout, setInterval, setImmediate (Node), DOM events, network I/O |
Low priority. Only one macrotask is executed per event loop iteration. |
6. Practical Example
This script demonstrates how promise callbacks (microtasks) execute before timers (macrotasks), even if the timer has a delay of 0ms:
7. Common Mistakes
- Starving the Macrotask Queue: Adding microtasks recursively inside a microtask callback creates an infinite loop that keeps the Microtask Queue filled. This starves the event loop, preventing macrotasks (like timers) from running and blocking DOM rendering updates.
8. Quick Quiz
Q1: If a promise callback (microtask) schedules another promise callback, when will that new callback execute?
A) In the next event loop iteration, after running pending timers
B) In the current iteration, before any pending timers are executed
Answer: B — The Event Loop executes all pending microtasks in the queue, including any new ones added during the current run, before moving on to macrotasks or rendering.
9. Scenario-Based Challenge
The Priority Sequence Resolver:
Analyze the execution order of a script that schedules: setTimeout, Promise.resolve().then(), queueMicrotask(), and a synchronous console.log. Trace the state of both queues step-by-step to show the exact output sequence.
10. Debugging Exercise
Explain why the DOM text change is never visible, and how to fix it:
function updateUi() { const el = document.getElementById('title'); el.textContent = 'Processing...';
// Objective: Let user see 'Processing...' before starting heavy calculations queueMicrotask(() => { // Heavy synchronous calculation const start = Date.now(); while (Date.now() - start < 1000) {} el.textContent = 'Done!'; }); }
View Solution
Diagnosis: The heavy calculation is scheduled as a microtask. Since microtasks execute immediately after the synchronous code finishes and before DOM rendering updates, the browser does not render the 'Processing...' text update. Instead, the calculation runs and blocks the main thread, and then the text changes directly to 'Done!'.
Fix: Schedule the heavy calculation as a macrotask using setTimeout, which allows the browser to perform a DOM rendering update in between ticks:
function updateUi() { const el = document.getElementById('title'); el.textContent = 'Processing...';
setTimeout(() => { const start = Date.now(); while (Date.now() - start < 1000) {} el.textContent = 'Done!'; }, 0); // Allows DOM render to complete first }
11. Interview Questions
🟢 Q1: Describe the order of execution between microtasks and macrotasks in the Event Loop.
Answer:
1. Execute all synchronous code on the Call Stack.
2. When the Call Stack is empty, execute all microtasks in the Microtask Queue until it is completely empty.
3. Perform DOM rendering updates if needed.
4. Dequeue and execute the first task from the Macrotask Queue.
5. Repeat this loop, checking and emptying the Microtask Queue after every macrotask execution.
12. Production Considerations
- • queueMicrotask for Batching: Use
queueMicrotask()when you want to schedule a task to run immediately after the current synchronous block finishes, but before yielding control to browser rendering or timers. This is useful for batching operations to avoid redundant rendering cycles.