Node.js 10 🟢 Call Stack, Microtasks, Macrotasks, and process.nextTick
The call stack, microtask queue, and macrotask queue form the foundational memory and scheduling model that Node.js uses to execute JavaScript. Every function call creates a stack frame, every asynchronous callback is scheduled on one of the queues, and the event loop coordinates between them. Understanding these three components explains why process.nextTick runs before promises, why promises run before setTimeout, and why blocking the call stack freezes the entire application.
The distinction between microtasks and macrotasks is not merely academic. It determines the order in which callbacks execute, and getting that order wrong leads to subtle bugs: a callback that assumes a value has been set, a cleanup function that runs too late, or an event handler that fires before it is registered. The process.nextTick queue, which is unique to Node.js, adds a third layer that runs before both, and its existence reflects a deliberate design philosophy about API consistency.
This chapter covers the call stack and its role in function execution, the two queue types and their ordering rules, the unique behavior of process.nextTick, and the patterns that follow from understanding the scheduling model.
Key point: The call stack executes functions synchronously until it is empty. Then the process.nextTick queue is drained, followed by the microtask queue (promises), and only then does the event loop continue to the next phase where macrotasks (timers, I/O callbacks, setImmediate) execute. Recursively scheduling microtasks or nextTick callbacks starves the event loop.
Why the stack and queues exist
The synchronous execution problem. JavaScript executes code synchronously on a single thread. When a function calls another function, the caller must pause while the callee runs, then resume where it left off. The call stack is the data structure that tracks this: each function call pushes a frame, and each return pops one. This is what makes synchronous execution possible .
The asynchronous execution problem. Some operations cannot complete immediately. A network request takes time. A file read must wait for the disk. A timer must wait for the clock. These operations cannot run on the call stack without blocking it. Instead, they are scheduled as callbacks on queues, and the event loop executes them when the stack is empty .
The priority problem. Not all callbacks are equal. Some must run as soon as possible, before any I/O or timers. Others can wait for the next event loop iteration. The microtask queue and the nextTick queue provide this priority: they run after the current operation completes but before the event loop continues, giving high-priority callbacks a chance to execute first .
The API consistency problem. process.nextTick exists because APIs should behave consistently. An API that takes a callback should always call it asynchronously, even if the result is available synchronously. Without nextTick, an API would have to choose between calling the callback synchronously (breaking the contract) or deferring it with setTimeout (adding unnecessary latency). nextTick provides a way to defer the callback to the end of the current operation, preserving the asynchronous contract without a timer delay .
a. The call stack
The call stack is a LIFO data structure that tracks the execution context of functions. When a function is called, a new frame is pushed onto the stack containing the function’s arguments, local variables, and return address. When the function returns, the frame is popped, and control resumes in the caller .
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
console.log(square(5)); // 25
The execution sequence is:
square(5)is called, pushing a frame forsquare.squarecallsmultiply(5, 5), pushing a frame formultiply.multiplyreturns 25, popping its frame.squarereturns 25, popping its frame.console.log(25)executes.
The stack is empty when the script finishes. The event loop then processes the next queued callback, which creates a new stack .
Stack overflow. Recursive function calls that never return push frames indefinitely until the stack’s memory limit is reached, causing a RangeError: Maximum call stack size exceeded. This is why recursive functions must have a base case, and why deep recursion should be converted to iteration when possible .
Blocking the stack. A long-running synchronous operation keeps the stack occupied, and no other JavaScript can execute. A while(true) loop, a synchronous file read, or a heavy computation blocks everything: timers do not fire, I/O callbacks do not run, and the application becomes unresponsive .
b. Microtasks and macrotasks
Callbacks are scheduled on two types of queues: the microtask queue and the macrotask queue (often called the task queue).
Macrotasks are the coarser-grained tasks. Each event loop iteration processes at most one macrotask from the queue. Macrotasks include setTimeout, setInterval, setImmediate, I/O callbacks, and UI rendering (in browsers) .
Microtasks are the finer-grained tasks. After each macrotask completes, the microtask queue is drained completely before the event loop proceeds to the next macrotask. Microtasks include promise callbacks (then, catch, finally), queueMicrotask callbacks, and MutationObserver (in browsers) .
The key difference is that macrotasks yield to the event loop between each task, while microtasks do not yield until the queue is empty. This means a microtask that schedules another microtask will run it before any macrotask, potentially starving the event loop if the recursion is unbounded .
console.log('start');
setTimeout(() => console.log('macrotask'), 0);
Promise.resolve()
.then(() => console.log('microtask 1'))
.then(() => console.log('microtask 2'));
console.log('end');
// Output:
// start
// end
// microtask 1
// microtask 2
// macrotask
The synchronous code runs first (start, end). Then the microtask queue is drained (microtask 1, microtask 2). Finally, the macrotask executes (macrotask) .
c. process.nextTick
process.nextTick is a Node.js-specific scheduling function that adds callbacks to the nextTick queue. This queue is processed after the current operation completes but before the event loop continues to the next phase .
console.log('start');
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
console.log('end');
// Output:
// start
// end
// nextTick
// promise
The nextTick queue is drained before the microtask queue in CommonJS modules. In ES modules, the order is reversed because ESM loading itself runs in the microtask queue .
Why process.nextTick exists. The primary use case is API consistency. An API that takes a callback should always call it asynchronously, even if the result is known synchronously. Without nextTick, the API would have to choose between calling the callback synchronously (violating the async contract) or deferring it with setTimeout (adding latency and breaking synchronous error handling) .
function apiCall(arg, callback) {
if (typeof arg !== 'string') {
return process.nextTick(callback, new TypeError('argument should be string'));
}
// ... do async work
}
By using process.nextTick, the error is passed to the callback after the rest of the user’s code has executed, allowing the user to set up error handlers before the callback fires .
Starvation risk. Recursive process.nextTick calls can starve the event loop, preventing it from reaching the poll phase. This is a real risk that must be avoided in production code .
Complete Example Session
// ============================================
// PART 1: SYNCHRONOUS CALL STACK
// ============================================
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
console.log(multiply(add(2, 3), 4)); // 20
// ============================================
// PART 2: MACROTASK VS MICROTASK ORDER
// ============================================
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
// start, end, promise, timeout
// ============================================
// PART 3: PROCESS.NEXTTICK BEFORE PROMISE
// ============================================
console.log('start');
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
console.log('end');
// start, end, nextTick, promise
// ============================================
// PART 4: MICROTASK STARVATION
// ============================================
// This starves the event loop: the timeout never runs.
function recursiveMicrotask() {
Promise.resolve().then(recursiveMicrotask);
}
// recursiveMicrotask(); // Uncomment to see starvation
setTimeout(() => console.log('this never runs'), 0);
// ============================================
// PART 5: CALL STACK OVERFLOW
// ============================================
function recursive() {
recursive();
}
// recursive(); // RangeError: Maximum call stack size exceeded
// ============================================
// PART 6: FIXING STACK OVERFLOW WITH SETTIMEOUT
// ============================================
function recursiveSafe() {
setTimeout(recursiveSafe, 0);
}
// This does not overflow because each call
// starts a new stack in a new macrotask.
// ============================================
// PART 7: NEXT TICK FOR API CONSISTENCY
// ============================================
function asyncDouble(n, callback) {
if (typeof n !== 'number') {
return process.nextTick(callback, new Error('not a number'));
}
process.nextTick(callback, null, n * 2);
}
asyncDouble(5, (err, result) => {
if (err) throw err;
console.log(result); // 10
});
// ============================================
// PART 8: PROMISE VS NEXTTICK IN ESM
// ============================================
// In ESM, promise runs before nextTick.
// In a .mjs file:
// start, end, promise, nextTick
// ============================================
// PART 9: COMBINING ALL THREE
// ============================================
console.log('1: sync start');
process.nextTick(() => console.log('5: nextTick'));
Promise.resolve().then(() => console.log('6: microtask'));
setTimeout(() => console.log('8: macrotask'), 0);
console.log('2: sync end');
// 1, 2, 5, 6, 8
// ============================================
// PART 10: AVOIDING STARVATION
// ============================================
function processChunk(items, index) {
if (index >= items.length) return;
// Process one item
console.log(items[index]);
// Schedule next chunk as a macrotask, not a microtask
setImmediate(() => processChunk(items, index + 1));
}
// processChunk([1, 2, 3, 4, 5], 0);
These ten parts cover the call stack, microtask vs macrotask ordering, process.nextTick priority, starvation, stack overflow, fixing overflow with macrotasks, API consistency with nextTick, ESM ordering differences, combining all queues, and avoiding starvation with setImmediate.
Quick Reference
Queue Priority
| Queue | Runs | Examples |
|---|---|---|
| Call stack | Synchronously | Function calls |
nextTick queue | After current operation | process.nextTick |
| Microtask queue | After nextTick | Promises, queueMicrotask |
| Macrotask queue | Next event loop iteration | setTimeout, setImmediate, I/O |
Ordering in CommonJS
Sync code → nextTick → Microtasks → Macrotasks
Ordering in ESM
Sync code → Microtasks → nextTick → Macrotasks
Key Functions
| Function | Queue | Runs |
|---|---|---|
process.nextTick | nextTick | Before microtasks (CJS) |
Promise.then | Microtask | After nextTick (CJS) |
queueMicrotask | Microtask | Same as promise |
setTimeout | Macrotask | Next loop iteration |
setImmediate | Macrotask | Check phase |
| I/O callbacks | Macrotask | Poll phase |
Starvation Risks
| Recursive call | Result |
|---|---|
process.nextTick | Starves I/O |
Promise.then | Starves I/O |
setImmediate | Does not starve (yields to loop) |
setTimeout | Does not starve (yields to loop) |
Best Practices
✅ Do This:
process.nextTick(() => { /* defer to after current op */ });
Promise.resolve().then(() => { /* microtask */ });
setImmediate(() => { /* next loop iteration */ });
queueMicrotask(() => { /* portable microtask */ });
❌ Don’t Do This:
process.nextTick(recursiveNextTick); // ❌ Starves I/O
Promise.resolve().then(recursive); // ❌ Starves I/O
while (true) { /* ... */ } // ❌ Blocks the stack
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Callback runs too late | Used setTimeout instead of nextTick | Use process.nextTick |
| Callback runs before handler | Synchronous API | Use process.nextTick to defer |
| Event loop starved | Recursive microtasks or nextTick | Use setImmediate for recursion |
| Stack overflow | Deep recursion | Convert to iteration or setTimeout |
| Wrong order in ESM | ESM runs in microtask queue | Check CJS vs ESM ordering |
Real-World Examples
1. Defer to After Current Operation
process.nextTick(() => console.log('after current'));
2. API Consistency
function asyncFn(cb) {
process.nextTick(cb);
}
3. Promise Microtask
Promise.resolve().then(() => console.log('microtask'));
4. Portable Microtask
queueMicrotask(() => console.log('microtask'));
5. Next Loop Iteration
setImmediate(() => console.log('immediate'));
6. Timer
setTimeout(() => console.log('timeout'), 0);
7. Avoid Starvation
function processNext() {
setImmediate(processNext);
}
8. Check Order
console.log('1');
process.nextTick(() => console.log('2'));
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 1, 4, 2, 3
9. Stack Overflow Fix
function loop(n) {
if (n <= 0) return;
setImmediate(() => loop(n - 1));
}
10. Error Handling with nextTick
process.nextTick((err) => {
if (err) console.error(err);
}, new Error('deferred'));
Visual
Execution Order
┌──────────────────────────────────────────────────────────────┐
│ SYNCHRONOUS CODE (call stack) │
│ └── Runs to completion │
│ │ │
│ ▼ │
│ process.nextTick QUEUE │
│ └── Drained completely │
│ │ │
│ ▼ │
│ MICROTASK QUEUE (promises, queueMicrotask) │
│ └── Drained completely │
│ │ │
│ ▼ │
│ EVENT LOOP PHASES (macrotasks) │
│ ├── Timers (setTimeout, setInterval) │
│ ├── Poll (I/O) │
│ ├── Check (setImmediate) │
│ └── Close │
└──────────────────────────────────────────────────────────────┘
Queue Starvation
┌──────────────────────────────────────────────────────────────┐
│ NORMAL STARVED │
│ │
│ Stack: empty Stack: empty │
│ nextTick: [] nextTick: [] │
│ microtasks: [] microtasks: [recursive] │
│ macrotasks: [timeout] macrotasks: [timeout] │
│ │
│ Timeout runs Microtask keeps adding │
│ itself; timeout never runs │
└──────────────────────────────────────────────────────────────┘
Call Stack Overflow
┌──────────────────────────────────────────────────────────────┐
│ function recursive() { │
│ recursive(); │
│ } │
│ │
│ ┌─────────────┐ │
│ │ recursive │ ← frame 1 │
│ ├─────────────┤ │
│ │ recursive │ ← frame 2 │
│ ├─────────────┤ │
│ │ recursive │ ← frame 3 │
│ ├─────────────┤ │
│ │ ... │ │
│ ├─────────────┤ │
│ │ recursive │ ← frame N │
│ └─────────────┘ │
│ RangeError: Maximum call stack size exceeded │
└──────────────────────────────────────────────────────────────┘
CJS vs ESM Ordering
┌──────────────────────────────────────────────────────────────┐
│ COMMONJS ESM │
│ │
│ sync sync │
│ process.nextTick microtasks (promises) │
│ microtasks (promises) process.nextTick │
│ macrotasks macrotasks │
│ │
│ nextTick first promise first │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Call stack | LIFO structure for synchronous function calls |
| Macrotask | Coarse-grained task; one per loop iteration |
| Microtask | Fine-grained task; queue drained completely |
process.nextTick | Node.js-specific; runs after current operation |
| Queue order (CJS) | nextTick → microtasks → macrotasks |
| Queue order (ESM) | microtasks → nextTick → macrotasks |
| Starvation risk | Recursive microtasks or nextTick |
| Stack overflow | Deep recursion; fixed with setImmediate |
| Macrotask examples | setTimeout, setImmediate, I/O |
| Microtask examples | Promises, queueMicrotask |
Key takeaways:
- The call stack executes JavaScript synchronously until empty. Each function call pushes a frame, and each return pops one. Recursive calls without a base case cause stack overflow .
- Microtasks run before macrotasks. After each macrotask completes, the microtask queue is drained completely. Promises and
queueMicrotaskcallbacks are microtasks . process.nextTickruns before microtasks in CommonJS. It is not part of the event loop; it is processed after the current operation completes, before the loop continues .- In ESM, the order is reversed. Because ESM loading is itself a microtask, promise callbacks run before
process.nextTickcallbacks . - Recursive microtasks starve the event loop. A microtask that schedules another microtask will run it before any macrotask, potentially preventing timers and I/O from ever executing .
process.nextTickexists for API consistency. It allows an API to defer a callback to the end of the current operation, preserving the asynchronous contract without the latency ofsetTimeout.- Use
setImmediatefor recursive scheduling. Unlike microtasks,setImmediateyields to the event loop between iterations, preventing starvation .
Remember: The call stack, microtask queue, and macrotask queue form a priority system. Synchronous code runs first on the stack. When the stack empties, the nextTick queue is drained, then the microtask queue. Only then does the event loop proceed to the next macrotask. This ordering is deterministic and explains why promises resolve before timers, and why process.nextTick runs before promises in CommonJS. The danger is starvation: a microtask or nextTick callback that schedules itself recursively prevents the event loop from ever reaching the macrotask queue, blocking I/O and timers indefinitely. Use setImmediate when you need to schedule recursive work without starving the loop. Understanding this model is understanding how Node.js schedules every asynchronous operation.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!