| |

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:

  1. square(5) is called, pushing a frame for square.
  2. square calls multiply(5, 5), pushing a frame for multiply.
  3. multiply returns 25, popping its frame.
  4. square returns 25, popping its frame.
  5. 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

QueueRunsExamples
Call stackSynchronouslyFunction calls
nextTick queueAfter current operationprocess.nextTick
Microtask queueAfter nextTickPromises, queueMicrotask
Macrotask queueNext event loop iterationsetTimeout, setImmediate, I/O

Ordering in CommonJS

Sync code → nextTick → Microtasks → Macrotasks

Ordering in ESM

Sync code → Microtasks → nextTick → Macrotasks

Key Functions

FunctionQueueRuns
process.nextTicknextTickBefore microtasks (CJS)
Promise.thenMicrotaskAfter nextTick (CJS)
queueMicrotaskMicrotaskSame as promise
setTimeoutMacrotaskNext loop iteration
setImmediateMacrotaskCheck phase
I/O callbacksMacrotaskPoll phase

Starvation Risks

Recursive callResult
process.nextTickStarves I/O
Promise.thenStarves I/O
setImmediateDoes not starve (yields to loop)
setTimeoutDoes 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

PitfallWhy It HappensFix
Callback runs too lateUsed setTimeout instead of nextTickUse process.nextTick
Callback runs before handlerSynchronous APIUse process.nextTick to defer
Event loop starvedRecursive microtasks or nextTickUse setImmediate for recursion
Stack overflowDeep recursionConvert to iteration or setTimeout
Wrong order in ESMESM runs in microtask queueCheck 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

ItemValue
Call stackLIFO structure for synchronous function calls
MacrotaskCoarse-grained task; one per loop iteration
MicrotaskFine-grained task; queue drained completely
process.nextTickNode.js-specific; runs after current operation
Queue order (CJS)nextTick → microtasks → macrotasks
Queue order (ESM)microtasks → nextTick → macrotasks
Starvation riskRecursive microtasks or nextTick
Stack overflowDeep recursion; fixed with setImmediate
Macrotask examplessetTimeout, setImmediate, I/O
Microtask examplesPromises, 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 queueMicrotask callbacks are microtasks .
  • process.nextTick runs 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.nextTick callbacks .
  • 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.nextTick exists 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 of setTimeout .
  • Use setImmediate for recursive scheduling. Unlike microtasks, setImmediate yields 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!