process.nextTick and Microtasks
These two mechanisms have the highest priority in Node's execution order. Understanding them is crucial for predictable async behavior.
process.nextTick()
Not Part of the Event Loop
Despite its name, process.nextTick() is NOT part of the event loop. It runs between phases, before any phase begins.
setImmediate(() => console.log('1. Check phase'));
setTimeout(() => console.log('2. Timer phase'), 0);
process.nextTick(() => console.log('3. nextTick'));
console.log('4. Sync');
// Output: 4, 3, 2, 1 (or 4, 3, 1, 2)
// nextTick ALWAYS runs before any event loop phase
The nextTick Queue
All nextTick callbacks are processed before continuing:
process.nextTick(() => {
console.log('tick 1');
process.nextTick(() => console.log('tick 2'));
});
setTimeout(() => console.log('timeout'), 0);
// Output: tick 1, tick 2, timeout
// All nextTicks drain before timer phase
Warning: I/O Starvation
Recursive nextTick can starve the event loop:
// DON'T DO THIS:
function recurse() {
process.nextTick(recurse);
}
recurse();
// I/O callbacks NEVER run!
// setTimeout NEVER fires!
// Event loop is starved
Microtask Queue (Promises)
Promise Callbacks
Promise .then(), .catch(), and .finally() callbacks go to the microtask queue.
Promise.resolve().then(() => console.log('1. Promise'));
process.nextTick(() => console.log('2. nextTick'));
console.log('3. Sync');
// Output: 3, 2, 1
// nextTick has priority over microtasks
Microtasks vs nextTick Priority
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// Output: nextTick, promise
// nextTick queue drains BEFORE microtask queue
The Complete Priority Order
1. Synchronous code
2. process.nextTick queue (all callbacks)
3. Microtask queue (Promises)
4. Event loop phases (timers → poll → check → close)
setTimeout(() => console.log('1. timeout'), 0);
setImmediate(() => console.log('2. immediate'));
Promise.resolve().then(() => console.log('3. promise'));
process.nextTick(() => console.log('4. nextTick'));
console.log('5. sync');
// Output: 5, 4, 3, (1 or 2), (2 or 1)
Why nextTick Exists
Use Case 1: Emit Events After Constructor
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {
constructor() {
super();
// BAD: Event fires before handler can be attached
// this.emit('ready');
// GOOD: Defers emission until after constructor returns
process.nextTick(() => {
this.emit('ready');
});
}
}
const emitter = new MyEmitter();
emitter.on('ready', () => console.log('Ready!'));
// Output: Ready!
Use Case 2: Consistent Async Error Handling
function apiCall(arg, callback) {
if (typeof arg !== 'string') {
// BAD: Sync callback (zalgo!)
// callback(new TypeError('arg must be string'));
// GOOD: Always async
return process.nextTick(
callback,
new TypeError('arg must be string')
);
}
// Async operation...
someAsyncOp(arg, callback);
}
Use Case 3: Ensure Variable Initialization
let bar;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1 (not undefined)
});
bar = 1;
queueMicrotask()
Node also provides queueMicrotask() for adding to the microtask queue:
queueMicrotask(() => console.log('microtask'));
process.nextTick(() => console.log('nextTick'));
// Output: nextTick, microtask
// Same priority as Promises
Practical Patterns
Ensuring User Code Runs First
const fs = require('fs');
function wrapperRead(path, options, callback) {
return fs.readFile(path, options, (err, data) => {
// Ensure user's error handlers are attached
process.nextTick(() => {
callback(err, data);
});
});
}
Breaking Up Sync Work
function processBigArray(array, callback) {
const results = [];
let index = 0;
function processChunk() {
// Process 100 items
const end = Math.min(index + 100, array.length);
for (; index < end; index++) {
results.push(expensiveOperation(array[index]));
}
if (index < array.length) {
// Use setImmediate, not nextTick (to allow I/O)
setImmediate(processChunk);
} else {
callback(results);
}
}
// Start processing (defer to not block caller)
process.nextTick(processChunk);
}
Promise Interop
// Promises and nextTick interleave
process.nextTick(() => {
console.log('1');
Promise.resolve().then(() => console.log('2'));
process.nextTick(() => console.log('3'));
});
// Output: 1, 3, 2
// nextTick queue fully drains, THEN microtasks
When to Use Each
| Use Case | Tool |
|---|---|
| Ensure code runs after current function completes | process.nextTick() |
| Allow I/O to proceed before callback | setImmediate() |
| Chain async operations | Promise.then() |
| Quick async scheduling | queueMicrotask() |
The Names Are Backwards
As Node's documentation states:
We recommend developers use
setImmediate()in all cases because it's easier to reason about.
process.nextTick fires immediately (before next I/O).
setImmediate fires on the next tick (after I/O).
The names are historical accidents that can't be changed due to npm ecosystem compatibility.
Key Takeaways
- nextTick runs before any event loop phase
- Microtasks (Promises) run after nextTick, before phases
- Priority order: sync → nextTick → microtasks → event loop
- Use setImmediate to avoid I/O starvation
- nextTick is great for deferring until after current function
- Recursive nextTick can starve the event loop