delay
Tiny promise-based timer utility for JavaScript. Calling delay(milliseconds) returns an awaitable promise; options can carry a resolved value or an AbortSignal. Version 7 also exposes a random range delay, a way to settle a pending delay early, and a factory that accepts custom timer functions for tests. Its unusual feature is support for waits beyond the native timer limit. It works in browsers and modern Node, but Node-only code already has a standard promise timer.
A clean browser-and-Node timer helper with useful cancellation and test hooks. Skip it in Node-only applications unless you specifically need range delays, early settlement, custom timers, or waits beyond native timeout limits.
Use it if
- One code path must use the same cancellable promise delay in both browsers and Node
- You need delays longer than the native timer ceiling and want the package to schedule them safely
- Tests need an injectable timer implementation while application code keeps the same delay API
- You need rangeDelay or clearDelay often enough that a small shared helper is preferable to repeating it
- You target Node only: the README explicitly recommends node:timers/promises setTimeout for a standard awaitable delay
- Your runtime is Node 18 or older: delay 7.0.0 requires Node 20 and is published as ESM only
- You need to put a deadline around another promise: delay does not cancel or time out arbitrary work, so use AbortSignal.timeout, Promise.race with careful cleanup, or p-timeout
- You are writing deterministic tests: real sleeps and rangeDelay make suites slow and timing-sensitive unless you inject or fake timers
- You only need a one-line browser sleep once: new Promise(resolve => setTimeout(resolve, ms)) avoids adding this package and its two runtime dependencies
Setup reality
npm install delay is simple, and TypeScript declarations ship through the package export. The version 7 boundary matters: it requires Node 20 or newer and the package is ESM, so CommonJS require('delay') is not the supported import shape. Node-only projects probably should not install it because node:timers/promises already provides an awaitable setTimeout with value and AbortSignal options. Browser projects must have AbortController support if they use cancellation. An aborted delay rejects with an AbortError; forgetting to catch that rejection can become an unhandled rejection during shutdown or component cleanup. clearDelay does not reject or cancel the promise in the usual sense: the README says it clears the timer and settles the promise, so awaiting code proceeds with its configured value. It also silently does nothing for unrelated promises. rangeDelay adds nondeterministic timing and should not appear in ordinary unit tests unless that behavior is the subject of the test. createDelay is the escape hatch for fake timers, but pass a matched setTimeout and clearTimeout pair from the same clock implementation. Unlimited delays are implemented through a dependency rather than one native timeout, so they are scheduling convenience, not durable jobs; process restarts, suspended tabs, and machine sleep can still invalidate wall-clock expectations. Use a job queue or persisted timestamp for anything operationally important.
Patterns
Wait before continuingpause-async-flow
import delay from 'delay';
await delay(250);
await refreshStatus();For Node-only code, prefer setTimeout from node:timers/promises unless another delay feature is needed.
Resolve with a typed valueresolve-with-value
import delay from 'delay';
const state = await delay(100, { value: 'ready' });
console.log(state);The promise resolves to the exact value, and TypeScript infers its type from the option.
Cancel a pending waitabort-delay
import delay from 'delay';
const controller = new AbortController();
const waiting = delay(10_000, { signal: controller.signal });
controller.abort();
try {
await waiting;
} catch (error) {
if (error.name !== 'AbortError') throw error;
}Aborting rejects with AbortError; always handle that expected rejection.
Stop on cancellation or deadlinecombine-abort-signals
import delay from 'delay';
const signal = AbortSignal.any([
request.signal,
AbortSignal.timeout(5_000),
]);
await delay(1_000, { signal });AbortSignal.any and AbortSignal.timeout require current runtimes; this cancels the wait, not unrelated application work.
Wait for a random intervalrandomize-wait
import { rangeDelay } from 'delay';
await rangeDelay(500, 1_500);Random waits make tests nondeterministic. Use this for deliberate jitter, not as a substitute for synchronization.
Settle a delay earlyclear-pending-delay
import delay, { clearDelay } from 'delay';
const pending = delay(30_000, { value: 'continue' });
clearDelay(pending);
console.log(await pending);clearDelay settles rather than rejects the promise, so awaiting code continues immediately.
Create a delay bound to test timersinject-fake-timers
import { createDelay } from 'delay';
const testDelay = createDelay({
setTimeout: clock.setTimeout.bind(clock),
clearTimeout: clock.clearTimeout.bind(clock),
});
const pending = testDelay(1_000, { value: 'done' });
clock.tick(1_000);
await pending;Pass both timer functions from the same fake clock or clearDelay cannot reliably clear scheduled work.
Back off between bounded retriesretry-with-backoff
import delay from 'delay';
for (let attempt = 0; attempt < 4; attempt++) {
try {
return await request();
} catch (error) {
if (attempt === 3 || !isRetryable(error)) throw error;
await delay(250 * 2 ** attempt, { signal });
}
}Retry only idempotent operations or use an idempotency key; the timer does not make a repeated side effect safe.
Spread concurrent retry attemptsadd-retry-jitter
import { rangeDelay } from 'delay';
const cap = Math.min(10_000, 300 * 2 ** attempt);
await rangeDelay(0, cap, { signal });Full jitter reduces synchronized retries, but server Retry-After guidance should take priority when present.
Avoid the dependency in Node-only codeuse-native-node-timer
import { setTimeout as delay } from 'node:timers/promises';
await delay(250, 'ready', { signal });This is the README's recommended Node-only alternative and supports both a resolved value and AbortSignal.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| sleep-promise | npm | You need only a minimal promise sleep and do not need AbortSignal, random ranges, or custom timers |
| p-sleep | npm | A very small legacy-compatible sleep helper is enough and its older release profile fits your runtime |
| promise-timeout | npm | Your actual problem is rejecting work that exceeds a deadline rather than pausing before the next step |