process.nextTick and Microtasks

Every time you run JavaScript in Node.js, callbacks don’t just fire whenever — they’re processed through carefully ordered queues that run between and even inside phases of the event loop. process.nextTick() and microtasks (Promise callbacks and queueMicrotask()) are two such queues, and they run with a higher priority than timers, I/O callbacks, or setImmediate(). Understanding exactly when they fire is essential for writing correct asynchronous Node code, building libraries with predictable APIs, and debugging cases where “async” code somehow runs before code that looks like it should have finished first.

Overview: How Node Prioritizes These Queues

Node.js’s event loop, powered by libuv, cycles through phases: timers, pending callbacks, poll, check, and close callbacks. Between the synchronous portion of any operation and the next phase of the loop — and in fact between every single callback that Node executes — two extra queues are drained first: the process.nextTick queue and the microtask queue (which holds resolved and rejected Promise .then/.catch/.finally callbacks and anything scheduled with queueMicrotask()).

The order Node follows, every single time it has a chance to run more JavaScript, is:

  1. Run the currently executing synchronous code to completion.
  2. Drain the process.nextTick queue completely. If a nextTick callback schedules another nextTick callback, that new one runs too before moving on.
  3. Drain the microtask queue completely (Promise callbacks, queueMicrotask()). Same rule: if a microtask schedules another microtask, it runs before Node moves on.
  4. Only then does Node continue with the next phase of the event loop — running a timer callback, an I/O callback, or a setImmediate() callback.

process.nextTick() always runs before Promise microtasks, and both always run before setTimeout(), setInterval(), and setImmediate() — no matter how small the timer delay is, even 0. This differs from browsers, which don’t have process.nextTick() at all; it’s a Node-specific API that predates Promises, kept because a huge amount of core Node code and npm packages depend on its exact timing guarantees.

Why does this matter? Two big reasons:

1. API consistency. If a function sometimes calls its callback synchronously and sometimes asynchronously (for example, only calling back later if a file needs to be read first), that inconsistency causes subtle bugs — listeners added right after the call might miss a synchronous emit. Wrapping a callback in process.nextTick() guarantees it always fires on the next tick, never synchronously, so callers can rely on it running after they’ve finished attaching their handlers.

2. Starvation risk. Because Node fully drains the nextTick and microtask queues before doing anything else, code that keeps re-scheduling itself with process.nextTick() can prevent the event loop from ever reaching timers, I/O, or setImmediate() — a real, documented gotcha covered in Common Mistakes below.

Syntax

process.nextTick(callback, ...args);
queueMicrotask(callback);
Part Meaning
callback The function to run once the current operation completes, before the event loop continues.
...args (nextTick only) Extra arguments passed to callback when it runs — avoids creating a closure just to pass data.
process.nextTick Node-specific, highest priority. Not part of the Promise/microtask spec. Available globally without an import.
queueMicrotask Standard web API also available in Node and browsers. Schedules a callback on the same queue used by Promise callbacks.

Both are global — no require or import needed. Neither accepts a delay; they always mean “as soon as possible, before anything else.”

Examples

Example 1: Ordering of nextTick, promises, and timers

console.log("start");

setTimeout(() => {
  console.log("timeout");
}, 0);

process.nextTick(() => {
  console.log("nextTick 1");
});

Promise.resolve().then(() => {
  console.log("promise 1");
});

process.nextTick(() => {
  console.log("nextTick 2");
});

console.log("end");

Output:

start
end
nextTick 1
nextTick 2
promise 1
timeout

The two console.log calls at the top level run first, synchronously, printing start and end. Only after that synchronous code finishes does Node drain the nextTick queue — both nextTick callbacks run, in the order they were scheduled, before the promise callback gets a turn. The promise microtask runs next, and only after both queues are empty does Node move into the timers phase and run the setTimeout callback, even though it was scheduled with a 0ms delay before either nextTick call. This is the core rule to memorize: nextTick beats microtasks, and both beat timers.

Example 2: process.nextTick vs queueMicrotask

console.log("sync");

process.nextTick(() => {
  console.log("nextTick callback");
});

queueMicrotask(() => {
  console.log("microtask callback");
});

Output:

sync
nextTick callback
microtask callback

queueMicrotask() and a resolved Promise.then() share the same microtask queue, which runs immediately after the nextTick queue empties, but still before the event loop continues. Use process.nextTick() only when you specifically need Node’s stronger “runs before literally everything else” guarantee; otherwise prefer promises or queueMicrotask(), since they behave the same as in browsers and the rest of the JavaScript ecosystem.

Example 3: Using nextTick for a consistent, always-async API

const { EventEmitter } = require("node:events");

class Resource extends EventEmitter {
  constructor(path) {
    super();
    if (!path) {
      process.nextTick(() => {
        this.emit("error", new Error("path is required"));
      });
      return;
    }
    this.path = path;
    process.nextTick(() => {
      this.emit("ready", this.path);
    });
  }
}

const resource = new Resource();
resource.on("error", (err) => {
  console.error("Resource error:", err.message);
});

const goodResource = new Resource("/tmp/data.txt");
goodResource.on("ready", (path) => {
  console.log("Resource ready:", path);
});

Output:

Resource error: path is required
Resource ready: /tmp/data.txt

This is the classic reason process.nextTick() exists in Node’s own source. The Resource constructor needs to notify callers of success or failure, but it can’t emit synchronously inside the constructor — the caller hasn’t had a chance to attach an 'error' or 'ready' listener yet, because new Resource(...) hasn’t even returned. By deferring the emit with process.nextTick(), Node guarantees the listener is already attached by the time the event fires, whether construction “fails” or “succeeds” — the caller never has to guess whether to attach handlers before or after construction.

How It Works Step by Step: Watching a Chain Starve a Timer

This example makes the priority ordering concrete by chaining nextTick calls and racing them against a timer:

console.log("start");

let count = 0;

function scheduleNextTick() {
  process.nextTick(() => {
    count++;
    if (count < 5) {
      scheduleNextTick();
    } else {
      console.log("nextTick chain finished, count =", count);
    }
  });
}

scheduleNextTick();

setTimeout(() => {
  console.log("timeout fired");
}, 0);

console.log("end");

Output:

start
end
nextTick chain finished, count = 5
timeout fired

Walking through it:

  1. start and end print synchronously as the script’s top-level code runs.
  2. scheduleNextTick() queued one nextTick callback before the script finished; that callback increments count and, seeing it’s still below 5, calls scheduleNextTick() again — which queues another nextTick callback.
  3. Because Node fully drains the nextTick queue before doing anything else, all five links of this self-refilling chain run back-to-back, with the setTimeout callback locked out the entire time.
  4. Only once count reaches 5 and no further nextTick callback is scheduled does the queue finally empty, letting Node move into the timers phase and run the setTimeout callback.

With a bounded chain like this one, the delay is invisible. But if the chain never stops refilling itself, the timer — and every other phase of the event loop — never runs again. That’s exactly the bug in the next section.

Common Mistakes

Mistake 1: An unbounded nextTick recursion starves the event loop

Wrong:

function starveEventLoop() {
  process.nextTick(starveEventLoop);
}

starveEventLoop();

setTimeout(() => {
  console.log("this will never run");
}, 0);

This looks harmless, but it never lets go: every nextTick callback schedules another nextTick callback before it returns, and Node insists on draining the nextTick queue completely before continuing. The queue never empties, so the event loop can never reach the timers phase — the setTimeout callback is starved forever and the process appears to hang, all while technically “doing work.” (Do not run this snippet — it never terminates.)

Corrected — yield to the event loop instead of the nextTick queue:

function politeStep(n) {
  if (n <= 0) {
    console.log("chain done");
    return;
  }
  setImmediate(() => politeStep(n - 1));
}

politeStep(3);

setTimeout(() => {
  console.log("timeout can still run");
}, 0);

setImmediate() schedules its callback for the check phase of the event loop rather than the nextTick queue, so each step lets timers, I/O, and other pending work get a turn before the next recursive call runs. Use setImmediate() (or break work into batches processed across multiple event loop turns) whenever a “keep going” recursive callback needs to coexist with the rest of the application, not process.nextTick().

Mistake 2: Forgetting that a thrown error inside nextTick crashes the process

A callback passed to process.nextTick() runs outside of any try/catch the caller had in place, because by the time it runs the original call stack is long gone. If it throws and nothing is listening for process.on('uncaughtException'), Node prints the error and exits. Always wrap risky logic inside a nextTick callback in its own try/catch, or route errors through an event emitter’s 'error' event (as in Example 3) instead of a bare throw.

Mistake 3: Assuming setTimeout(fn, 0) behaves like process.nextTick(fn)

They are not interchangeable. setTimeout(fn, 0) still has to wait for the current nextTick queue and microtask queue to drain, and then for the event loop to reach the timers phase — which can be delayed by other pending timers or I/O. process.nextTick() runs before any of that. Code that depends on “run as soon as possible, before any I/O” should use process.nextTick() or a promise microtask, not a zero-delay timer.

Best Practices

  • Prefer queueMicrotask() or promises over process.nextTick() for ordinary “run this async” logic — they behave the same as in browsers and are easier for other developers to reason about.
  • Reach for process.nextTick() specifically when building a library and you need to guarantee a callback or event never fires synchronously (see Example 3), or when you need Node’s absolute highest-priority queue.
  • Never build an unconditionally self-scheduling process.nextTick() loop — always give it a base case, or use setImmediate() for “keep working across turns” logic instead.
  • Always attach an error handler before anything that might emit one asynchronously, and wrap nextTick callback bodies in try/catch if they can throw.
  • Don’t use process.nextTick() to defer heavy synchronous computation — it still runs on the main thread and still blocks the loop while it executes; it only changes when it runs, not how it runs.
  • Remember the priority order when debugging surprising log output: synchronous code, then all nextTicks, then all microtasks, then timers, I/O, and setImmediate.

Practice Exercises

  1. Write a script that logs "A" synchronously, schedules "B" with setTimeout(fn, 0), schedules "C" with process.nextTick(), schedules "D" with Promise.resolve().then(), and logs "E" synchronously. Predict the printed order before running it, then verify.
  2. Build a small EventEmitter subclass (like the Resource example) that reads a config object passed to its constructor and emits either 'ready' or 'error' on process.nextTick(). Confirm that a listener attached immediately after construction always receives the event.
  3. Take the broken “unbounded nextTick recursion” example from Common Mistakes and rewrite it two different ways: once using setImmediate(), and once using a plain counter that stops recursing after a fixed number of iterations. Compare how quickly the accompanying setTimeout callback fires in each version.

Summary

  • process.nextTick() queues a callback to run immediately after the current operation finishes, before the event loop continues — it is Node’s highest-priority queue.
  • Promise callbacks and queueMicrotask() share a separate microtask queue that runs right after the nextTick queue drains, but still before timers, I/O, or setImmediate().
  • The guaranteed order is: synchronous code, then all nextTick callbacks (including ones scheduled by other nextTick callbacks), then all microtasks, then the rest of the event loop.
  • Use process.nextTick() to guarantee a callback or event never fires synchronously, most commonly inside libraries and EventEmitter-based APIs.
  • Never let a nextTick callback unconditionally reschedule itself — it can starve timers and I/O indefinitely; use setImmediate() instead when you need to yield back to the event loop.
  • setTimeout(fn, 0) is not a substitute for process.nextTick() — it runs strictly later, after both the nextTick and microtask queues are empty.