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

  1. nextTick runs before any event loop phase
  2. Microtasks (Promises) run after nextTick, before phases
  3. Priority order: sync → nextTick → microtasks → event loop
  4. Use setImmediate to avoid I/O starvation
  5. nextTick is great for deferring until after current function
  6. Recursive nextTick can starve the event loop