Wait without blocking in Node.js

Pause an async function with a timer Promise, cancel a wait with AbortSignal, and avoid blocking the event loop or assuming exact timing.

Best practices
Illustration of an event loop and asynchronous tasks

Await a timer Promise to pause an async function while allowing the event loop to continue handling other work.

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
async function example() {
  console.log('Starting');
  await sleep(3000);
  console.log('Continuing');
}
example().catch(console.error);

This suspends the async function at await; it does not suspend the entire Node process.

Use the built-in Promise timer

node:timers/promises was added in Node 15 and became stable in Node 16. In an ES module:

import { setTimeout as sleep } from 'node:timers/promises';
await sleep(3000);
const result = await sleep(1000, 'finished');
console.log(result);

In CommonJS, obtain it with require('node:timers/promises') and put the await inside an async function. CommonJS does not permit top-level await.

Space operations deliberately

Inside an async function, given an iterable items and an async processItem function:

for (const item of items) {
  await processItem(item);
  await sleep(500);
}

Each operation completes before the next delay begins. This is sequential pacing, not a complete rate limiter across multiple workers or replicas. The final iteration also waits; omit that delay if your workflow does not need it.

An async callback passed to forEach() can use await internally, but forEach() does not await the Promises it returns. Use for...of for this sequential behavior, or an explicitly managed concurrency strategy for parallel work.

Cancel a wait

import { setTimeout as sleep } from 'node:timers/promises';
const controller = new AbortController();
const waiting = sleep(5000, undefined, { signal: controller.signal });
controller.abort();
try {
  await waiting;
} catch (error) {
  if (error.name !== 'AbortError') throw error;
  console.log('Wait canceled');
}

Cancellation rejects the timer Promise; it does not automatically cancel unrelated work that started before the wait. The timer normally keeps the event loop alive. With ref: false, it may not finish if nothing else keeps the process running.

Treat timing as approximate

Do not busy-wait with a loop comparing Date.now(): it occupies the executing JavaScript thread and prevents that event loop from handling requests. A worker thread can isolate CPU work, but moving work there does not turn timers into real-time guarantees.

Node does not guarantee an exact callback time. Busy event loops cause delays, and out-of-range, fractional, or invalid delay values are normalized. In particular, very large delays do not provide an arbitrarily long sleep. Validate application delays and use a durable scheduler for long-lived deadlines.

For tests, inject a sleep function or use your test framework's fake timers. Check that the test advances both timers and the relevant Promise work before asserting completion.

References

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting