Async patterns, timers and the event loop
Promises and async/await discipline, the timer functions, scheduling order between nextTick, microtasks and setImmediate, and cancellation.
What the loop actually runs
The event loop is a sequence of phases. Each phase runs its callbacks, then the microtask queue drains completely before the next phase begins. Blocking a phase blocks everything.
| Phase | What runs there | Reach for it when |
|---|---|---|
| timers | setTimeout and setInterval callbacks | You need a delay, not a precise schedule |
| pending callbacks | Deferred I/O errors from the previous turn | Rarely visible in application code |
| poll | Most I/O callbacks — the loop spends its time here | Anything you do with files, sockets or databases |
| check | setImmediate callbacks | You want to run right after the current poll phase |
| close callbacks | Socket and handle close events | Cleanup bookkeeping |
console.log("1 sync");
setTimeout(() => console.log("5 timeout"), 0);
setImmediate(() => console.log("6 immediate"));
queueMicrotask(() => console.log("3 microtask"));
process.nextTick(() => console.log("2 nextTick"));
Promise.resolve().then(() => console.log("4 promise"));
// sync, then nextTick, then promise/microtask,
// then timeout and immediate — which of the last two first is not guaranteed
// at the top level, but inside an I/O callback setImmediate always wins.Think in microtasks and macrotasks rather than in milliseconds. Promise callbacks and queueMicrotask are microtasks; timers, I/O and setImmediate are macrotasks. A microtask that schedules another microtask can starve the loop entirely, because the microtask queue always drains completely before the next phase.
Timers and async discipline
import { setTimeout as sleep } from "node:timers/promises";
const id = setTimeout(() => console.log("late"), 5000);
clearTimeout(id);
// unref() lets the process exit even though the timer is pending
setTimeout(() => console.log("breathe"), 100).unref();
const started = performance.now();
await sleep(250); // awaitable, no callback pyramid
console.log(performance.now() - started);- A timer fires no earlier than the delay and later if the loop is busy, so never use
setTimeoutto measure elapsed time — useperformance.now(). - Sequential
await sleepcalls guarantee a minimum gap, not a steady rate; drift accumulates over many iterations. node:timers/promisesgives yousleep,setIntervalandscheduler.waitthat all accept anAbortSignal.- Unhandled promises inside a timer callback are floating promises — nothing reports them unless you handle rejections globally.
Cancellation and failures
const controller = new AbortController();
const { signal } = controller;
await fetch(url, { signal });
await sleep(1000, { signal });
controller.abort(new Error("deadline exceeded")); // the reason is preserved
process.on("unhandledRejection", (reason) => {
console.error("unhandled rejection", reason);
process.exitCode = 1;
});- Since Node 15 an unhandled rejection terminates the process. Add a handler only to log and exit deliberately, never to swallow the failure.
- Pass the same
AbortSignaldown through every layer of a request; a cancelled parent that leaves children running leaks sockets and timers. Promise.allrejects on the first failure but does not cancel the rest, so sibling work keeps running — useallSettledwhen every result matters.- Always
awaitor return a promise. A floating promise loses both its result and its stack context.
⚠️
An
async function that fails inside Array.forEach or a bare event listener produces a rejection nobody sees until the process dies. Use for...of with await, or wrap the listener body in a try/catch that reports.FAQ
When should I use process.nextTick instead of setImmediate?
Almost never in application code.
nextTick runs before promise callbacks and can starve I/O if used recursively; setImmediate yields to the poll phase and is the safer way to defer work.How do I run many async operations without opening thousands of sockets?
Bound the concurrency. Run a fixed number of workers pulling from a shared queue, or use a small pool helper, so in-flight requests stay near what the downstream service can handle.
Related
Events, streams and buffers Modules, ESM and CommonJS
Last refreshed 2026-09-18.