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:
- Run the currently executing synchronous code to completion.
- Drain the
process.nextTickqueue completely. If a nextTick callback schedules another nextTick callback, that new one runs too before moving on. - Drain the microtask queue completely (Promise callbacks,
queueMicrotask()). Same rule: if a microtask schedules another microtask, it runs before Node moves on. - 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:
startandendprint synchronously as the script’s top-level code runs.scheduleNextTick()queued one nextTick callback before the script finished; that callback incrementscountand, seeing it’s still below 5, callsscheduleNextTick()again — which queues another nextTick callback.- 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
setTimeoutcallback locked out the entire time. - Only once
countreaches 5 and no further nextTick callback is scheduled does the queue finally empty, letting Node move into the timers phase and run thesetTimeoutcallback.
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 overprocess.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 usesetImmediate()for “keep working across turns” logic instead. - Always attach an
errorhandler before anything that might emit one asynchronously, and wrap nextTick callback bodies intry/catchif 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
- Write a script that logs
"A"synchronously, schedules"B"withsetTimeout(fn, 0), schedules"C"withprocess.nextTick(), schedules"D"withPromise.resolve().then(), and logs"E"synchronously. Predict the printed order before running it, then verify. - Build a small
EventEmittersubclass (like theResourceexample) that reads a config object passed to its constructor and emits either'ready'or'error'onprocess.nextTick(). Confirm that a listener attached immediately after construction always receives the event. - 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 accompanyingsetTimeoutcallback 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, orsetImmediate(). - 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 andEventEmitter-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 forprocess.nextTick()— it runs strictly later, after both the nextTick and microtask queues are empty.
