JavaScript Event Loop: Microtasks, Macrotasks, and Runtime Architecture
1. Core Intuition: The Restaurant Kitchen Analogy
Before mathematical proofs, understand the Event Loop as a restaurant:
- The Call Stack (The Waiter): There is only one waiter. They can only take one order at a time.
- Web APIs (The Kitchen): The waiter drops off complex tasks (like fetching network data or waiting for a timer) to the kitchen staff so the waiter can immediately go back to taking more orders.
- The Macrotask Queue (The Order Window): When the kitchen finishes a meal, they place it on the window.
- The Event Loop (The Manager): The manager constantly checks: "Is the waiter idle? If yes, take the next finished meal from the window and give it to the waiter to serve."
Basic Trace Example
Consider this simple setTimeout execution:
console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");
// Output: 1, 3, 2
The setTimeout is sent to the kitchen. The waiter immediately moves on to print "3". The "2" is placed in the queue, and served only after the waiter is completely idle.
Promise Queueing vs Race
In interviews, you must know how Promises batch. Promise.all([p1, p2]) waits for all to resolve before queueing the microtask, whereas Promise.race([p1, p2]) queues a microtask the moment the fastest promise finishes.
In production, always attach a global error handler for unhandled rejections using process.on('unhandledRejection') to prevent silent crashes.
2. Abstract and First Principles
The Event Loop is the foundational execution model of JavaScript. It resolves the fundamental paradox of the language: how can a runtime execution environment with a single-threaded Call Stack handle high-concurrency tasks such as network I/O, file system reads, DOM interactions, and timers without blocking the main execution thread?
From first principles, concurrency in computer science is typically managed via multi-threading and OS-level context switching. JavaScript circumvents thread synchronization costs (locks, mutexes, race conditions) by delegating blocking operations to the underlying environment (Browser C++ APIs or Node.js libuv worker threads) and multiplexing the completion of these operations back onto the single main thread via an infinite loop: The Event Loop.
This chapter presents a rigorous, textbook-level audit of the Event Loop. We will analyze memory models, trace execution frames, compare language paradigms (Python asyncio, Go), prove algorithmic complexity bounds of task queues, and examine esoteric runtime edge cases.
3. V8 Memory Model and Runtime Architecture
Before dissecting the Event Loop, we must map the precise memory model of the JavaScript runtime.
3.1 The Memory Heap and Call Stack
When JavaScript executes, the V8 engine allocates memory and tracks execution contexts.
- Memory Heap: An unstructured region of memory used for dynamic allocation of objects, arrays, and closures. Variables not allocated on the stack (primitives) reside here, governed by V8's Garbage Collector (Orinoco / Scavenger).
- Call Stack: A LIFO (Last-In-First-Out) data structure that records the active execution frames. Each time a function is invoked, an Execution Context is pushed. When the function returns, it is popped.
3.2 Environmental Bindings (Web APIs / C++ APIs)
The Call Stack only executes synchronous JavaScript. Asynchronous operations like fetch(), setTimeout(), and document.addEventListener() are NOT part of the JavaScript language specification (ECMAScript). They are Host APIs.
When setTimeout is called:
- The V8 engine pushes the
setTimeoutexecution frame onto the Call Stack. - The function invokes a C++ Web API provided by the Browser (or C++ binding in Node.js).
- The Web API spins up a background timer independently of the JavaScript thread.
- The
setTimeoutframe pops off the Call Stack immediately. - Upon completion, the Web API pushes the callback into the Task Queues.
3.3 The Three Asynchronous Queues
The Event Loop orchestrates between the Call Stack and three distinct queues:
graph TD
A[V8 Call Stack] -->|Synchronous Execution| B{Is Stack Empty?}
B -- Yes --> C[Microtask Queue]
B -- No --> A
C -->|Promises, queueMicrotask| D{Is Microtask Queue Empty?}
D -- No --> C
D -- Yes --> E[Render Pipeline Check]
E -->|60Hz Check| F[RequestAnimationFrame & UI Update]
F --> G[Macrotask Queue]
E -. Skip Render .-> G
G -->|setTimeout, I/O, UI Events| H[Execute ONE Macrotask]
H --> A
4. Microtasks vs. Macrotasks: Formal Definitions
The WHATWG HTML Standard formally defines two classes of asynchronous tasks.
4.1 Macrotasks (Tasks)
A Task (or Macrotask) represents a discrete, independent unit of work. The Event Loop processes exactly one Macrotask per iteration before yielding to the Microtask Queue and Render Pipeline.
Sources of Macrotasks:
setTimeout()/setInterval()- User Interactions (
click,mousemove,keydown) - Network Events (XHR, fetch response streams)
setImmediate()(Node.js specific)- MessageChannel and postMessage
4.2 Microtasks
A Microtask is a high-priority task scheduled to run immediately after the currently executing script and before any rendering or Macrotask execution. Crucial Rule: The Event Loop will drain the entire Microtask Queue until it is completely empty. If a Microtask enqueues another Microtask, it will execute in the same cycle, potentially starving the event loop.
Sources of Microtasks:
Promise.resolve().then(),.catch(),.finally()queueMicrotask()MutationObserverprocess.nextTick()(Node.js specific, runs before standard microtasks)
5. WHATWG HTML Standard: The Algorithmic Specification
The Event Loop's complexity can be formalized via the WHATWG HTML standard. Let be the Macrotask Queue, be the Microtask Queue, and be the Render Pipeline.
The Event Loop processing algorithm runs continuously:
- Task Extraction: Let be the oldest task in . If is empty, wait until a task is added.
- Execute Macrotask: Set the currently running task to . Run the task (pushing frames to the Call Stack).
- Remove: Remove from .
- Microtask Checkpoint:
- While is not empty:
- Let be the oldest microtask.
- Execute on the Call Stack.
- Remove from .
- While is not empty:
- Render Check: If the runtime is a browser and the rendering epoch (~16.6ms for 60fps) has elapsed:
- Execute
requestAnimationFramecallbacks. - Recalculate Styles.
- Reflow/Layout.
- Paint.
- Execute
- Return to 1.
Complexity Proof: UI Starvation by Microtasks
Let be the execution time of a single microtask. Let be the number of microtasks in . The total time spent in Step 4 is . If a microtask generator continuously adds microtasks such that , . Because Step 5 (Render) and Step 1 (Next Macrotask) strictly wait for Step 4 to terminate, the rendering pipeline frequency drops to Hz. This mathematically proves that infinite microtasks yield a completely frozen UI, whereas infinite macrotasks still allow UI rendering between task executions.
6. Exhaustive Execution Trace Analysis
Async/Await Transformation to Promise Chains
When Babel or V8 compiles async/await, it transforms the syntax into an implicit Promise chain and a state machine. Understanding this mapping is critical for event loop tracing.
Animation Sequence (4 Frames of Execution):
- Yield: When the engine hits
await, it evaluates the right-hand expression and wraps it inPromise.resolve(). - Suspend: The
asyncfunction's execution context is immediately suspended and popped off the Call Stack. Control yields back to the caller. - Resolve to Microtask: The engine implicitly attaches a
.then()to the resolved Promise. The continuation of the function is placed in the Microtask Queue. - Resume via next(value): Once the Event Loop drains the Microtask Queue, it restores the function's execution context, passing the resolved value back in (similar to a Generator's
.next(value)), and resumes execution.
Let us analyze a highly complex asynchronous code snippet that mixes Promise, setTimeout, async/await, and standard synchronous execution. We will trace the memory stack and queues precisely.
The Target Code
console.log("A");
setTimeout(() => console.log("B"), 0);
async function foo() {
console.log("C");
await Promise.resolve();
console.log("D");
}
foo();
Promise.resolve().then(() => {
console.log("E");
setTimeout(() => console.log("F"), 0);
}).then(() => console.log("G"));
console.log("H");
Trace Matrix
| Step | Call Stack | Microtask Queue () | Macrotask Queue () | Console Output | State Description |
|---|---|---|---|---|---|
| 1 | console.log("A") | [] | [] | A | Synch code runs. |
| 2 | setTimeout() | [] | [B] | - | API sets timer, enqueues Macrotask B. |
| 3 | foo() | [] | [B] | C | foo runs synchronously up to await. |
| 4 | await Promise | [D] | [B] | - | await suspends foo. Remainder (D) queued as Microtask. |
| 5 | Promise.then() | [D, E] | [B] | - | then(E) queued as Microtask. |
| 6 | console.log("H") | [D, E] | [B] | H | Synch code finishes. Call Stack empties! |
| 7 | (Draining ) | [E] | [B] | D | D executes from . |
| 8 | (Draining ) | [] | [B, F] | E | E executes, calling setTimeout(F), pushing F to . |
| 9 | (Promise chaining)| [G] | [B, F] | - | Since E resolved, its chained then(G) pushes G to . |
| 10 | (Draining ) | [] | [B, F] | G | G executes. is now completely empty. |
| 11 | (Next Macrotask) | [] | [F] | B | Event loop picks oldest Macrotask B. |
| 12 | (Next Macrotask) | [] | [] | F | Event loop picks next Macrotask F. |
Final Output Order: A, C, H, D, E, G, B, F
7. Edge Cases and Node.js Event Loop Anomalies
While the browser event loop is standardized by WHATWG, Node.js implements its event loop via libuv, which introduces distinct phases.
7.1 Node.js libuv Phases
Node.js executes macrotasks in specific phases, continuously circling:
- Timers:
setTimeout,setInterval. - Pending Callbacks: I/O callbacks deferred to the next loop iteration.
- Idle, Prepare: Internal use only.
- Poll: Retrieve new I/O events; execute I/O related callbacks.
- Check:
setImmediate()callbacks execute here. - Close Callbacks: e.g.,
socket.on('close', ...).
7.2 The setImmediate vs setTimeout(..., 0) Anomaly
Consider this Node.js script:
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
Edge Case: If executed in the main module, the output order is non-deterministic. The performance of the system dictates if the timer expires before the loop enters the Timer phase.
However, if placed within an I/O callback, setImmediate is guaranteed to execute first because the Check phase immediately follows the Poll phase.
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate")); // ALWAYS prints first
});
7.3 process.nextTick() Priority
process.nextTick() is a Node.js specific mechanism that bypasses standard microtasks. A "NextTick Queue" exists and is drained before the standard Promise Microtask Queue.
Promise.resolve().then(() => console.log("Promise"));
process.nextTick(() => console.log("NextTick"));
// Output: NextTick, Promise
7.4 Node.js Event Loop Lag Monitoring
In production Node.js servers, measuring "Event Loop Lag" (the time between a timer being scheduled and it actually executing) is critical for health checks. The perf_hooks module provides precise measurements.
Case Study:
A server under heavy load was failing health checks. Using monitorEventLoopDelay, the team tracked the P99 latency of the event loop.
const { monitorEventLoopDelay } = require('perf_hooks');
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
// Log the P99 lag every 5 seconds
setInterval(() => {
const p99 = histogram.percentile(99);
console.log(`P99 Event Loop Lag: ${(p99 / 1e6).toFixed(2)}ms`);
if (p99 > 100_000_000) { // > 100ms lag
console.warn('CRITICAL: Event loop is overloaded!');
}
histogram.reset();
}, 5000);
By analyzing the lag, they discovered a synchronous regex evaluation causing 200ms blocks on the event loop, completely starving incoming HTTP requests.
8. Cross-Language Paradigm Comparison
To master the JavaScript Event Loop, we must contrast it with other concurrency models.
8.1 Python asyncio
Python's asyncio utilizes an explicit event loop. Unlike JavaScript's implicit loop tied to the runtime, Python requires the developer to fetch and run the loop.
import asyncio
async def main():
print("A")
await asyncio.sleep(0) # Yields to the event loop (Macrotask equivalent)
print("B")
asyncio.run(main())
Python distinguishes between Coroutines, Tasks, and Futures. Python's asyncio does not have a strict equivalent to a "Microtask Queue" that interrupts the event loop globally; tasks are scheduled cooperatively and context-switch strictly at await boundaries.
8.2 Go Goroutines
Go completely abstractions the event loop via the Go Scheduler and Goroutines (M:N scheduling).
package main
import (
"fmt"
"time"
)
func main() {
go func() { fmt.Println("B") }() // Spawns lightweight thread
fmt.Println("A")
time.Sleep(time.Millisecond) // Forces context switch
}
JavaScript is strictly 1:1 execution on the main thread, handling concurrency via queues. Go uses M OS threads : N Goroutines, allowing parallel execution, which JavaScript cannot do natively without Web Workers.
9. Browser Rendering Pipeline Integration
The rendering pipeline is intrinsically linked to the Event Loop. Browsers aim for a smooth 60 Frames Per Second (FPS), allocating ~16.6ms per frame.
9.1 The Render Epoch
If an Event Loop iteration (Macrotask + Microtask drain) takes longer than 16.6ms, the browser misses a frame, resulting in jank.
9.2 requestAnimationFrame (rAF)
requestAnimationFrame is a specialized queue that runs precisely before the rendering steps (Style, Layout, Paint).
setTimeout(() => console.log("Macrotask"), 0);
requestAnimationFrame(() => console.log("rAF"));
Promise.resolve().then(() => console.log("Microtask"));
Output Order:
- Microtask
- rAF (Assuming the epoch triggers render)
- Macrotask
Note: In Safari, rAF might execute after rendering, leading to subtle cross-browser spec deviations.
10. Exhaustive Interview Questions
Question 1: How can you implement a non-blocking heavy computation without Web Workers?
Answer: You can chunk the heavy computation using the Event Loop. By processing a small subset of the array and scheduling the next subset via setTimeout or requestAnimationFrame, you yield the Call Stack back to the Event Loop, allowing the browser to render and process user input.
function processChunk(data, index, chunkSize) {
let end = Math.min(index + chunkSize, data.length);
for (let i = index; i < end; i++) {
compute(data[i]);
}
if (end < data.length) {
setTimeout(() => processChunk(data, end, chunkSize), 0);
}
}
Question 2: What is the minimum delay of setTimeout(..., 0) according to the WHATWG spec?
Answer: The HTML5 spec dictates that nested setTimeout calls (depth > 5) will be clamped to a minimum delay of 4 milliseconds. This prevents CPU spin-locking by malicious scripts.
Question 3: Explain the difference between queueMicrotask(fn) and setTimeout(fn, 0).
Answer: queueMicrotask schedules a task in the Microtask Queue, executing immediately after the current synchronous block and before any rendering. setTimeout places it in the Macrotask Queue, forcing it to wait until the next event loop tick, allowing rendering and other macrotasks to interleave.
Question 4: Does a DOM mutation trigger a Microtask or a Macrotask?
Answer: Modern DOM mutations observed by a MutationObserver trigger a Microtask. However, native DOM events (like a user clicking a button) are queued as Macrotasks. Interestingly, if you manually call button.click() via JavaScript, the event handlers are executed synchronously on the Call Stack!
Question 5: Prove mathematically why infinite recursion with Promise.resolve().then(...) blocks the browser while infinite setTimeout does not.
Answer: The Event Loop algorithm dictates: while (MicrotaskQueue.length > 0) { dequeue().execute(); }. If execution adds to the Microtask Queue, length never reaches 0. Thus, the algorithm never reaches Step 5 (Render). In contrast, Macrotasks are executed 1 per loop. The algorithm processes one setTimeout, proceeds to Step 4 (empty Microtasks), then Step 5 (Render), ensuring the UI remains responsive.
11. Conclusion
The JavaScript Event Loop is a triumph of asynchronous system design. By mastering the strict ordering of the Call Stack, Microtask Queue, Render Pipeline, and Macrotask Queue, developers can write deterministic, high-performance applications free of race conditions and UI jank. Understanding the deviations in V8, Node.js libuv, and WHATWG specifications is the hallmark of a Senior/Staff level JavaScript engineer.
Projects
In this section, we'll build projects that actively demonstrate the event loop in action.
-
Visual Event Loop Simulator: Build a React or Vanilla JS application that visually represents the Call Stack, Web APIs, Macrotask Queue, and Microtask Queue.
- Step 1: Create a UI layout with separate boxes for each queue and stack.
- Step 2: Implement a parser that takes simple JS snippets (with
setTimeout,Promise.then,console.log) and generates a sequence of execution steps. - Step 3: Use CSS animations and
requestAnimationFrameto step through the execution visually, moving function blocks from the Call Stack to Web APIs, then to Queues, and back to the Call Stack. - Goal: Help users internalize the exact order of execution visually and interactively.
-
Non-Blocking Data Processor: Build a Node.js script that processes a massive dataset (e.g., parsing a 5GB CSV file) without blocking the event loop.
- Step 1: Read the file using streams rather than loading it entirely into memory.
- Step 2: Use
setImmediateorsetTimeoutto chunk the processing of data rows so that other tasks (like responding to HTTP requests in an Express server running in the same process) can still be handled concurrently. - Step 3: Measure the latency of HTTP requests while the processing is ongoing to ensure the event loop remains responsive.
- Goal: Understand practical implications of event loop blocking and how to avoid it in production Node.js applications.
-
Custom Promise Polyfill: Implement a basic Promise class from scratch.
- Step 1: Define the state machine (pending, fulfilled, rejected).
- Step 2: Implement the
.then()method to handle chained callbacks. - Step 3: Use
queueMicrotaskto ensure that callbacks are executed asynchronously as microtasks, matching the native specification. - Goal: Understand how promises hook into the microtask queue at a fundamental level.
Assignments
-
Assignment 1: Trace the Output
- Deliverable: A written document detailing the exact console output order of a complex asynchronous code snippet provided by the instructor, along with a step-by-step breakdown of the Call Stack, Microtask Queue, and Macrotask Queue at each tick. You must explain why each log appears when it does.
-
Assignment 2: Unblock the Event Loop
- Deliverable: A refactored version of a given CPU-intensive synchronous function (e.g., calculating Fibonacci numbers recursively for large inputs) into an asynchronous, non-blocking version using chunking and timers. The refactored code must include tests proving that it doesn't freeze the browser UI or prevent other scripts from executing.
-
Assignment 3: Node.js Phase Mapping
- Deliverable: Write a Node.js script that logs messages using
fs.readFile,setTimeout,setImmediate,process.nextTick, andPromise. Structure the code so that you can reliably predict and explain the output based onlibuv's event loop phases. Write a brief report analyzing the output sequence and how it differs from browser behavior.
- Deliverable: Write a Node.js script that logs messages using
-
Assignment 4: Microtask Starvation
- Deliverable: Create a small web page that demonstrates the difference between Macrotask UI rendering and Microtask starvation. Write two functions: one that recursively calls
setTimeout, and another that recursively callsPromise.resolve().then(). Include a CSS animation on the page and document the observable effects on the animation when each function is executed.
- Deliverable: Create a small web page that demonstrates the difference between Macrotask UI rendering and Microtask starvation. Write two functions: one that recursively calls
Debugging Guide
When dealing with asynchronous JavaScript, debugging can become notoriously difficult due to the non-linear execution flow. Here are common bugs and how to fix them.
Common Bug 1: Unhandled Promise Rejections
- Symptom: A background task fails silently, or Node.js crashes with an
UnhandledPromiseRejectionWarning. - Fix: Always append
.catch()to your Promise chains or wrapawaitcalls intry...catchblocks. The event loop handles rejected promises asynchronously, so traditional synchronoustry...catcharound a function returning a Promise will not catch the error.
Common Bug 2: UI Freezing (Main Thread Blocking)
- Symptom: The browser tab becomes unresponsive, animations stutter, and buttons cannot be clicked.
- Fix: You have a long-running synchronous task or an infinite loop of microtasks (e.g., a Promise chain that never terminates). Use Chrome DevTools Performance tab to record a profile. Look for long yellow bars indicating heavy scripting. Refactor by breaking the heavy task into smaller chunks using
setTimeoutor offload it entirely to a Web Worker.
Common Bug 3: Race Conditions in Async Logic
- Symptom: Data renders incorrectly because an older network request resolves after a newer one, overwriting the UI state.
- Fix: Since the event loop processes callbacks as they arrive, network latency can cause out-of-order execution. Implement cancellation logic (e.g., using
AbortControllerforfetchrequests) or maintain a strictly incrementing request ID, discarding responses that do not match the latest ID.
Common Bug 4: Memory Leaks with Timers and Closures
- Symptom: Application memory usage grows continuously over time until it eventually crashes.
- Fix: Forgetting to call
clearTimeoutorclearIntervalkeeps the callback referenced in the host environment, preventing garbage collection of any variables captured in its closure. Always clean up timers, especially in single-page applications or React components (e.g., returning cleanup functions inuseEffect).
Testing Strategy
Testing asynchronous code requires specific strategies to ensure the event loop resolves tasks predictably during the test execution.
-
Async/Await in Tests: Modern testing frameworks like Jest, Mocha, and Vitest natively support Promises. Always return the Promise or use
async/awaitin your test definitions. This ensures the test runner waits for the microtask queue to drain before asserting the results.test('fetches data successfully', async () => { const data = await fetchData(); expect(data).toBeDefined(); }); -
Fake Timers for Macrotasks: Testing code that relies on
setTimeoutorsetIntervalcan make test suites extremely slow and flaky. Use the testing framework's fake timer utilities (e.g.,jest.useFakeTimers()). This allows you to synchronously "fast-forward" time and flush macrotasks without waiting for actual clock time to elapse.jest.useFakeTimers(); test('delays execution', () => { const callback = jest.fn(); delayedFunction(callback); jest.advanceTimersByTime(1000); // Instantly flushes the timer expect(callback).toHaveBeenCalled(); }); -
Flushing Promises: Sometimes you need to assert UI state after Promises have resolved but before timers fire. Since microtasks resolve before macrotasks, you can create a helper to flush the microtask queue in your tests. A common trick is awaiting a new Promise or using
setImmediate(in Node environments) to yield the tick.const flushPromises = () => new Promise(setImmediate); -
Testing UI Non-Blocking Behavior: To verify that a function doesn't block the UI, write a test that initiates the heavy function, immediately triggers a small asynchronous task (like a 10ms timeout), and checks if the small task resolves within a reasonable threshold (e.g., less than 50ms). If it takes seconds, the heavy function is improperly blocking the event loop.
FAQs
Q: Does JavaScript run in multiple threads?
A: JavaScript execution itself is strictly single-threaded; it operates entirely on one Call Stack. However, the runtime environment (the Browser or Node.js) utilizes multiple threads in the background (like C++ Web APIs or libuv worker pools) to handle I/O, networking, and timers asynchronously.
Q: Why does setTimeout(fn, 0) not execute immediately?
A: A delay of 0 does not mean "execute instantly." It means "execute as soon as possible after the current synchronous code finishes and the Call Stack is empty." Furthermore, the callback is placed in the Macrotask Queue, so it must also wait for the Microtask Queue to drain completely first.
Q: What is the difference between setImmediate and process.nextTick in Node.js?
A: process.nextTick runs callbacks immediately after the current operation completes, bypassing the standard event loop phases and even executing before standard Promise microtasks. setImmediate places callbacks in the "Check" phase of the Node.js event loop, which executes after the "Poll" phase (I/O callbacks).
Q: Can Web Workers bypass the single-thread limitation?
A: Yes. Web Workers allow you to run separate JavaScript execution contexts in completely separate threads. They have their own distinct Event Loop and Memory Heap, and communicate with the main thread strictly via message passing (postMessage), ensuring no shared memory race conditions.
Q: Is requestAnimationFrame a Macrotask or a Microtask?
A: It is technically neither. It belongs to a specialized rendering queue that the Event Loop checks right before performing the UI rendering steps. It executes after microtasks but before the next macrotask, aligned with the browser's refresh rate (typically ~16.6ms).
Revision Notes / Cheat Sheet
Use this reference table to quickly recall where different APIs fit within the Event Loop architecture.
| Feature / API | Queue Type | Execution Priority | Context |
|---|---|---|---|
| Synchronous Code | Call Stack | Highest (Runs Immediately) | Everywhere |
| Promise.then/catch | Microtask | Very High (Drains entirely after Call Stack empties) | Browser & Node |
| queueMicrotask() | Microtask | Very High (Drains entirely after Call Stack empties) | Browser & Node |
| process.nextTick() | NextTick | Ultra High (Runs before Microtasks) | Node.js Only |
| MutationObserver | Microtask | Very High (Batches DOM changes) | Browser Only |
| setTimeout() | Macrotask | Normal (Executes one per loop iteration) | Browser & Node |
| setInterval() | Macrotask | Normal (Executes one per loop iteration) | Browser & Node |
| DOM Events (clicks) | Macrotask | Normal (Queued by user interaction) | Browser Only |
| setImmediate() | Check Phase | Runs after Poll phase (I/O) | Node.js Only |
| requestAnimationFrame | Render Queue | Runs immediately before browser painting | Browser Only |
Key Rules to Remember:
- The Call Stack must be completely empty before any queue is checked.
- The Microtask Queue is always completely drained until empty before moving on.
- If a Microtask creates another Microtask, it executes in the same cycle (can block rendering).
- The Macrotask Queue executes exactly one task per iteration.
- In the Browser, UI rendering occurs between the Microtask drain and the next Macrotask.