Timers

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Node.js 26 (26.10.0, released 2026-09-21, the Current line that becomes LTS on 2026-10-28), with the differences of Node.js 24 "Krypton" (24.21.0, Active LTS) and Node.js 22 "Jod" (22.23.3, Maintenance) called out; the stability levels and version notes were checked against the official documentation on 2026-10-03, and every example below was run on all three lines unless the text says otherwise. A timer asks the runtime to call a function later. In Node.js the timer functions are globals, so no import is needed, and they are built around the event loop that is explained in The Event Loop: a timer callback runs in the timers phase, setImmediate() callbacks run in the check phase, and neither is a promise of an exact time. The Timers module (setTimeout(), setInterval(), setImmediate(), the clear* functions, and the Timeout and Immediate classes) is Stability 2 (Stable). The node:timers/promises API is Stable as well (it graduated from experimental in v16.0.0), with two exceptions: timersPromises.scheduler.wait() and timersPromises.scheduler.yield() are Stability 1 (Experimental), because they implement a draft web standard. timeout.close() is Stability 3 (Legacy), and timeout[Symbol.dispose]() and immediate[Symbol.dispose]() are Stable since v24.2.0 but are still documented as Experimental on Node.js 22. The JavaScript-language view of the same functions is in Standard Library: Console, URL & Timers, and the promise vocabulary used below is explained in Callbacks, Promises & async/await. The running scenario is the _Bookshelf application that grows across this section.

The timer functions at a glance

Table 1. The timer API and its stability (Node.js 26 documentation)
API Stability Notes

setTimeout(callback[, delay[, …​args]])

2 Stable

Runs callback once after about delay ms. Returns a Timeout. Default delay 1.

setInterval(callback[, delay[, …​args]])

2 Stable

Runs callback repeatedly. Returns a Timeout. Default delay 1.

setImmediate(callback[, …​args])

2 Stable

Runs callback in the check phase, after the I/O callbacks of the current loop iteration. Returns an Immediate.

clearTimeout(), clearInterval(), clearImmediate()

2 Stable

Cancel a timer. The first two accept a Timeout or its numeric or string primitive.

timeout.ref(), timeout.unref(), timeout.hasRef()

2 Stable

Control whether a timer keeps the process alive. hasRef() was added in v11.0.0.

timeout.refresh()

2 Stable

Restarts the countdown of the same object. Added in v10.2.0.

timeout[Symbol.dispose](), immediate[Symbol.dispose]()

2 Stable on 24.2+ and 26, Experimental on 22

Cancel the timer, which lets a timer be owned by a using declaration. Added in v20.5.0 and v18.18.0.

timeout.close()

3 Legacy

Use clearTimeout() instead.

node:timers/promises: setTimeout(), setImmediate(), setInterval()

2 Stable

Promise and async-iterable variants. Added in v15.0.0 (setInterval() in v15.9.0).

timersPromises.scheduler.wait(), timersPromises.scheduler.yield()

1 Experimental

Added in v17.3.0 and v16.14.0. Equivalent to setTimeout() and setImmediate() from node:timers/promises.

setTimeout, setInterval and setImmediate

setTimeout() schedules a one-time call, setInterval() schedules a call that repeats until it is cancelled, and setImmediate() schedules a call for the check phase of the current loop iteration, which is why it takes no delay. All three accept extra arguments that are passed to the callback, which avoids creating a closure just to carry values (the Learn article shows the same setTimeout(myFunction, 2000, firstParam, secondParam) form). setTimeout() and setInterval() return a Timeout object and setImmediate() returns an Immediate; both can be passed to the matching clear* function, and a cancelled timer never runs. Passing something that is not a function throws a TypeError with code ERR_INVALID_ARG_TYPE (since v18.0.0; earlier lines used ERR_INVALID_CALLBACK).

Scheduling, repeating and cancelling timers (basics.mjs)
const started = performance.now();
const elapsed = () => `${Math.round(performance.now() - started)} ms`;

// setTimeout: one-shot, extra arguments are passed to the callback
const timeout = setTimeout((title, year) => {
  console.log(`timeout fired at ~${elapsed()}: ${title} (${year})`);
}, 50, 'Dune', 1965);

// setInterval: repeats until cleared
let ticks = 0;
const interval = setInterval(() => {
  ticks += 1;
  console.log(`tick ${ticks} at ~${elapsed()}`);
  if (ticks === 3) clearInterval(interval);
}, 20);

// setImmediate: runs after the current poll phase, no delay argument
const immediate = setImmediate(() => console.log('immediate'));

// A timer that is cancelled before it fires never runs
const cancelled = setTimeout(() => console.log('never printed'), 10);
clearTimeout(cancelled);

console.log(timeout.constructor.name, immediate.constructor.name);
console.log('scheduled');
Output on Node.js 26, 24 and 22 (timings vary by a millisecond or two)
Timeout Immediate
scheduled
immediate
tick 1 at ~21 ms
tick 2 at ~42 ms
timeout fired at ~51 ms: Dune (1965)
tick 3 at ~63 ms

The Learn article also describes a "zero delay" setTimeout(): the callback is not run synchronously, it runs as soon as possible after the current code has finished. In Node.js a delay of 0 is not literally zero, because the minimum is 1 ms and smaller values are replaced by 1 (see Timer accuracy, drift and limits).

Recursive setTimeout instead of setInterval

setInterval() fires on a fixed schedule whether or not the previous callback has finished. With asynchronous work in the callback (a request, a database query) the runs can overlap and pile up. The pattern the Learn article calls recursive setTimeout schedules the next run only after the current one has completed, so runs never overlap and there is always a full delay between the end of one run and the start of the next. The async iterator of node:timers/promises (below) avoids the overlap too, but it does not guarantee that pause: the underlying interval keeps ticking while the loop body works, and a tick that has already elapsed is delivered as soon as the body asks for the next value, so when the work takes longer than the interval the iterations run back to back.

setInterval callbacks overlap; the async-iterable interval and recursive setTimeout do not (poll.mjs)
import { setInterval as every } from 'node:timers/promises';

const work = () => new Promise((r) => setTimeout(r, 60)); // slow work: 60 ms
const since = (t) => `~${Math.round(performance.now() - t)} ms`;

// 1. Async-iterable interval: no overlap, but ticks that elapsed during the work
//    are delivered at once, so the next iteration starts with no pause.
let t = performance.now();
let i = 0;
for await (const _ of every(20)) {
  i += 1;
  console.log(`iteration ${i} starts at ${since(t)}`);
  await work();
  if (i === 3) break;
}

// 2. Recursive setTimeout: the next run is scheduled only after the work finished,
//    so there is always a full 20 ms pause between runs.
t = performance.now();
let k = 0;
await new Promise((done) => {
  const run = async () => {
    k += 1;
    console.log(`run ${k} starts at ${since(t)}`);
    await work();
    if (k === 3) return done();
    setTimeout(run, 20);
  };
  setTimeout(run, 20);
});

// 3. Classic setInterval: keeps firing while the previous work is still pending
t = performance.now();
let j = 0;
const id = setInterval(async () => {
  j += 1;
  console.log(`callback ${j} starts at ${since(t)}`);
  if (j === 5) clearInterval(id); // stop scheduling; callbacks already started still finish
  await work();
}, 20);
Output on Node.js 26, 24 and 22 (timings vary by a millisecond or two)
iteration 1 starts at ~21 ms
iteration 2 starts at ~82 ms
iteration 3 starts at ~144 ms
run 1 starts at ~21 ms
run 2 starts at ~104 ms
run 3 starts at ~186 ms
callback 1 starts at ~20 ms
callback 2 starts at ~42 ms
callback 3 starts at ~62 ms
callback 4 starts at ~82 ms
callback 5 starts at ~103 ms

The three patterns differ in what happens after 60 ms of work with a 20 ms period. The for await loop never runs two iterations at once, but each iteration starts about 62 ms after the previous one, that is, right after the work finished: the ticks that fell due during the work were waiting, so there is no 20 ms gap. The recursive setTimeout() starts each run about 82 ms after the previous one (60 ms of work plus the full 20 ms delay), which is the pattern to use when a pause between runs matters, for example to avoid hammering an API that is already slow. The setInterval() callbacks start every 20 ms regardless, so up to three of them are in flight at the same time.

setTimeout(0) versus setImmediate

Both setTimeout(fn, 0) and setImmediate(fn) mean "as soon as possible", and the difference is the loop phase they belong to. From the main module the order is not deterministic, because it depends on how long the process took to start and on the 1 ms timer resolution: the example below printed the timeout first on the Node.js 24 and 22 runs and the immediate first on most Node.js 26 runs, and both orders were observed across repeated runs of Node.js 26. Inside an I/O callback the order is fixed: the loop is already past the timers phase, so the immediate always runs first. The full phase-by-phase explanation, including process.nextTick() (Stability 3, Legacy) and queueMicrotask(), is in The Event Loop.

The ordering of setTimeout(0) and setImmediate (order.mjs)
import { readFile } from 'node:fs';

setTimeout(() => console.log('main: timeout'), 0);
setImmediate(() => console.log('main: immediate'));

readFile(import.meta.filename, () => {
  setTimeout(() => console.log('io: timeout'), 0);
  setImmediate(() => console.log('io: immediate'));
});
Possible output (the first two lines can swap from run to run)
main: immediate
main: timeout
io: immediate
io: timeout

ref, unref, hasRef and refresh

By default every active Timeout and Immediate is ref’ed: the process does not exit while it is pending. An interval that is never cleared therefore keeps a script running forever. unref() tells the loop that the timer alone is not a reason to stay alive, which is what you want for housekeeping such as a heartbeat, a metrics flush or a cache sweeper that must not delay shutdown. ref() reverts it, hasRef() reports the current state, and calling ref() or unref() repeatedly has no extra effect. All three exist on both Timeout and Immediate and return the object, so they chain. timeout.refresh() resets the start time to now and reschedules the callback for the original duration, without allocating a new object; calling it on a timer that has already fired re-arms it, which makes it a cheap idle-timeout primitive (reset the timer on every sign of activity).

An unref’ed heartbeat, a ref’ed job and a refreshed idle timeout (refunref.mjs)
const heartbeat = setInterval(() => console.log('heartbeat'), 30);
heartbeat.unref();
console.log('hasRef after unref:', heartbeat.hasRef());

const work = setTimeout(() => console.log('work done'), 100);
console.log('hasRef of work:', work.hasRef());

// refresh() restarts the countdown of the same Timeout object
const idle = setTimeout(() => console.log('idle timeout reached'), 80);
setTimeout(() => idle.refresh(), 50); // pushes the idle deadline to ~130 ms
const t0 = performance.now();
idle.ref();
process.on('exit', () => console.log(`exit after ~${Math.round(performance.now() - t0)} ms`));
Output on Node.js 26, 24 and 22
hasRef after unref: false
hasRef of work: true
heartbeat
heartbeat
heartbeat
work done
heartbeat
idle timeout reached
exit after ~132 ms

The process ends right after the last ref’ed timer (the refreshed idle timeout, due at about 130 ms, not at 80 ms), even though the heartbeat interval would still be pending. The Timeout also has a Symbol.toPrimitive method (added in v14.9.0 and v12.19.0) that returns a number, so clearTimeout(+timeout) works, which mirrors the browser where setTimeout() returns a number; the number is only valid in the thread that created the timer. Because clearTimeout() and clearInterval() are interchangeable in Node.js, either can cancel either kind of timer, but use the matching one for readability.

Cancelling with Symbol.dispose and using

Both timer classes have a [Symbol.dispose]() method that cancels the timer, equivalent to clearTimeout() or clearImmediate(). It makes a timer usable with the using declaration of explicit resource management, which cancels it when the block ends. The method itself exists on all three lines (it was added in v20.5.0 and v18.18.0), but the using syntax is a SyntaxError on Node.js 22 (the V8 12.4 that Node.js 22 ships does not parse it), so on 22 call the method directly. The topic is covered with await using and AbortSignal in Async Context & Cancellation.

Cancelling timers through Symbol.dispose; the using block requires Node.js 24 or later (dispose.mjs)
const timeout = setTimeout(() => console.log('timeout ran'), 20);
const immediate = setImmediate(() => console.log('immediate ran'));
timeout[Symbol.dispose]();
immediate[Symbol.dispose]();
console.log('both cancelled, typeof:', typeof timeout[Symbol.dispose]);

{
  using t = setTimeout(() => console.log('never runs: disposed at block end'), 20);
}
Output on Node.js 26 and 24 (on Node.js 22 delete the using block first, because it is a SyntaxError there)
both cancelled, typeof: function

node:timers/promises

node:timers/promises offers setTimeout(), setImmediate() and setInterval() that return promises, so a delay can be await`ed instead of nested in a callback. It is a separate module, imported from `node:timers/promises (a built-in, so the node: prefix is the canonical spelling). Each function takes an optional value that the promise resolves with and an options object with two fields: signal, an AbortSignal that cancels the wait and rejects the promise with an AbortError (err.code is ABORT_ERR), and ref, which when false lets the process exit while the promise is pending. Note that the argument order differs from the callback API (setTimeout(delay, value, options), with no callback).

Awaiting delays, cancelling with an AbortSignal, async-iterable interval and the scheduler (promises.mjs)
import { setTimeout as sleep, setImmediate as tick, setInterval as every, scheduler } from 'node:timers/promises';

// 1. A promise that resolves with a value after a delay
const value = await sleep(50, 'dune');
console.log('slept, value =', value);

// 2. Cancellation with an AbortSignal rejects with AbortError
const ac = new AbortController();
const pending = sleep(10_000, 'never', { signal: ac.signal });
setTimeout(() => ac.abort(), 20);
try {
  await pending;
} catch (err) {
  console.log(err.name, err.code);
}

// 3. AbortSignal.timeout() as a deadline for the wait
try {
  await sleep(10_000, undefined, { signal: AbortSignal.timeout(30) });
} catch (err) {
  console.log(err.name, err.code);
}

// 4. Async-iterable interval: backpressure-friendly polling, stopped with break
let n = 0;
for await (const startedAt of every(25, Date.now())) {
  n += 1;
  console.log(`poll ${n} after ~${Math.round((Date.now() - startedAt) / 25) * 25} ms`);
  if (n === 3) break;
}

// 5. scheduler.wait() and scheduler.yield()
await scheduler.wait(20);
await scheduler.yield();
await tick();
console.log('done');
Output on Node.js 26, 24 and 22
slept, value = dune
AbortError ABORT_ERR
AbortError ABORT_ERR
poll 1 after ~25 ms
poll 2 after ~50 ms
poll 3 after ~75 ms
done

setInterval() from node:timers/promises returns an async iterator, so it is consumed with for await and stopped with break (which cleans up the underlying timer) or with a signal. The documentation points out that with ref: true you must call next() (explicitly or through for await) to keep the event loop alive. scheduler.wait(delay, options) is the same as timersPromises.setTimeout(delay, undefined, options) and scheduler.yield() is the same as timersPromises.setImmediate(); yield() is the idiomatic way to give other work a turn while splitting a long synchronous loop into chunks (see Don’t Block the Event Loop). Both scheduler functions implement the Scheduling APIs draft and are Stability 1 (Experimental), so they may change, whereas the three timer functions are Stable. The cancellation topic goes further in Async Context & Cancellation.

ref: false and the unsettled top-level await

A pending ref: false promise does not keep the process alive. In an ES module with a top-level await this means the module never finishes, the loop empties, and Node.js exits with code 13 and prints a warning about an unsettled top-level await.

A non-ref’ed wait does not keep the process alive (refopt.mjs)
import { setTimeout as sleep } from 'node:timers/promises';

// With ref: false the pending promise does not keep the process alive,
// so Node.js exits before the promise ever settles.
process.on('exit', (code) => console.log('exit code', code));
await sleep(60_000, undefined, { ref: false });
console.log('unreachable');
Output on Node.js 26, 24 and 22 (the warning also prints the file location)
Warning: Detected unsettled top-level await at file:///.../refopt.mjs:6
await sleep(60_000, undefined, { ref: false });
^

exit code 13

A Bookshelf poller with a deadline

Timers, AbortSignal.timeout() and AbortSignal.any() compose into a bounded retry loop. The AbortSignal passed to the sleep means that when the deadline (or the caller) aborts, the pending timer is cleared and no timer is left behind.

Polling until ready, with a per-attempt delay, a global deadline and cancellation (bookshelf.mjs)
import { setTimeout as sleep } from 'node:timers/promises';

// Bookshelf: poll a catalogue source until it is ready, with a per-attempt delay,
// an overall deadline, and cancellation. No timer is left behind after an abort.
let attempts = 0;
async function fetchStatus() {          // stand-in for a real request
  attempts += 1;
  return attempts >= 4 ? 'ready' : 'pending';
}

export async function waitUntilReady({ everyMs = 25, timeoutMs = 1_000, signal } = {}) {
  const deadline = AbortSignal.any([AbortSignal.timeout(timeoutMs), ...(signal ? [signal] : [])]);
  while (true) {
    if (await fetchStatus() === 'ready') return attempts;
    await sleep(everyMs, undefined, { signal: deadline });   // rejects with AbortError when the deadline fires
  }
}

console.log('ready after', await waitUntilReady(), 'attempts');

attempts = -1_000_000;                  // never ready
try {
  await waitUntilReady({ everyMs: 20, timeoutMs: 100 });
} catch (err) {
  console.log('gave up:', err.name);
}
Output on Node.js 26, 24 and 22
ready after 4 attempts
gave up: AbortError

Timer accuracy, drift and limits

The setTimeout() documentation is explicit: the callback "will likely not be invoked in precisely delay milliseconds", Node.js makes no guarantee about exact timing or about the ordering of callbacks, and the callback is called as close to the requested time as possible. These are the practical consequences.

Table 2. What to expect from Node.js timers
Aspect Behaviour

Minimum delay

When delay is larger than 2147483647, less than 1 or NaN, it is set to 1, and non-integer delays are truncated to an integer. A setTimeout(fn, 0) therefore waits at least 1 ms. (The Node.js 22 documentation does not list NaN, but NaN also behaves as 1 there.)

Lateness

A timer fires when the timers phase finds it due, so it is normally late by the time the loop spent elsewhere: running other callbacks, a long synchronous function, or the OS scheduler. Measured lateness of an idle process was around one millisecond.

A blocked loop

Synchronous work delays every timer. A timer due at 10 ms ran at 100 ms after a 100 ms busy loop. Measure this with monitorEventLoopDelay() from node:perf_hooks.

Interval drift

setInterval() re-arms relative to the current time, so a series of ticks accumulates a small error. Twenty ticks of 10 ms took about 210 ms in the run below, not 200 ms. Use timestamps or a recursive timer if the long-term rate matters.

Maximum delay

2147483647 ms, which is 2 to the power of 31 minus 1, about 24.8 days. Larger delays are not honoured: Node.js emits a TimeoutOverflowWarning and runs the callback after 1 ms.

Worker threads

A Timeout primitive is only usable in the thread that created it.

Measuring lateness, blocked-loop delay and interval drift (accuracy.mjs)
// 1. Timers are best effort: measure how late they actually fire
function lateness(delay) {
  return new Promise((resolve) => {
    const t0 = performance.now();
    setTimeout(() => resolve(performance.now() - t0 - delay), delay);
  });
}
console.log('lateness of a 0 ms timer :', (await lateness(0)).toFixed(2), 'ms (delay 0 is clamped to 1)');
console.log('lateness of a 10 ms timer:', (await lateness(10)).toFixed(2), 'ms');

// 2. A busy main thread delays every timer
const t0 = performance.now();
setTimeout(() => console.log('timer due at 10 ms ran at', Math.round(performance.now() - t0), 'ms'), 10);
while (performance.now() - t0 < 100); // block for 100 ms

// 3. Drift of setInterval over 20 ticks of 10 ms
let ticks = 0;
const start = performance.now();
const id = setInterval(() => {
  ticks += 1;
  if (ticks === 20) {
    clearInterval(id);
    console.log('20 ticks of 10 ms took', Math.round(performance.now() - start), 'ms');
  }
}, 10);
Output on Node.js 26 (24 and 22 are within a millisecond or two)
lateness of a 0 ms timer : 1.28 ms (delay 0 is clamped to 1)
lateness of a 10 ms timer: 1.09 ms
timer due at 10 ms ran at 100 ms
20 ticks of 10 ms took 214 ms

Delays beyond the 32-bit limit

Because the delay is stored in a 32-bit signed integer, a longer wait must be built from several shorter ones. On all three lines the overflow case below emits TimeoutOverflowWarning and the callback runs after about 1 ms. On Node.js 24 and 26, NaN and negative delays also emit a TimeoutNaNWarning and a TimeoutNegativeWarning (observed in these runs; these warnings are not listed in the timers documentation), and on Node.js 22 they are silently treated as 1.

Out-of-range delays are replaced by 1 ms and warned about (limits.mjs)
const MAX = 2 ** 31 - 1; // 2147483647 ms, about 24.8 days
console.log('largest delay:', MAX, 'ms =', (MAX / 86_400_000).toFixed(1), 'days');

process.on('warning', (w) => console.log(`warning: ${w.name}: ${w.message}`));

const t0 = performance.now();
// Larger than 2^31-1: the delay is reset to 1 ms and the callback runs almost at once
setTimeout(() => console.log('overflow timer ran after', Math.round(performance.now() - t0), 'ms'), MAX + 1);
// Fractions are truncated; NaN and negative delays become 1
setTimeout(() => console.log('fraction ran'), 10.9);
setTimeout(() => console.log('NaN ran'), NaN);
setTimeout(() => console.log('negative ran'), -5);
Output on Node.js 26 (warning text shortened; Node.js 22 prints only the overflow warning)
largest delay: 2147483647 ms = 24.9 days
warning: TimeoutOverflowWarning: 2147483648 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
warning: TimeoutNaNWarning: NaN is not a number.
Timeout duration was set to 1.
warning: TimeoutNegativeWarning: -5 is a negative number.
Timeout duration was set to 1.
overflow timer ran after 0 ms
NaN ran
negative ran
fraction ran

A wait of weeks or months is better handled by storing a deadline (in a database or a queue) than by one process-lifetime timer; a long timer is lost on every restart. For in-process use, re-arm timers no longer than the limit until the deadline has passed.

Waiting for an arbitrary duration by re-arming shorter timers (longwait.mjs)
import { setTimeout as sleep } from 'node:timers/promises';

const MAX_DELAY = 2 ** 31 - 1;

// Wait for any duration by re-arming timers no longer than maxDelay until the deadline passes
export async function sleepLong(ms, { maxDelay = MAX_DELAY, signal } = {}) {
  const deadline = Date.now() + ms;
  let rounds = 0;
  for (let left = ms; left > 0; left = deadline - Date.now()) {
    rounds += 1;
    await sleep(Math.min(left, maxDelay), undefined, { signal });
  }
  return rounds;
}

// Demonstrated with a tiny cap so that the loop is visible
const rounds = await sleepLong(130, { maxDelay: 40 });
console.log(`waited 130 ms in ${rounds} rounds of at most 40 ms`);
Output on Node.js 26, 24 and 22
waited 130 ms in 4 rounds of at most 40 ms

Scheduling that must survive restarts, run on a calendar or fan out across processes belongs to a job scheduler or queue, covered in Background Jobs, Scheduling & Queues.

Testing code that uses timers

Waiting for real time makes tests slow and flaky. The built-in test runner can replace timers with a mock whose clock only moves when the test says so: mock.timers.enable() (or t.mock.timers.enable(), which is restored automatically when the test ends) takes the list of APIs to mock, and tick() advances the clock. The mock can also fake Date. The test runner is covered in Testing with the Node.js Test Runner and Mocking, Snapshots & Coverage.

Testing a debounced save without waiting (mock.test.mjs)
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';

test('a debounced save runs once, 300 ms after the last call', (t) => {
  t.mock.timers.enable({ apis: ['setTimeout'] });   // restored automatically after the test
  const save = mock.fn();
  let handle;
  const debounced = () => { clearTimeout(handle); handle = setTimeout(save, 300); };

  debounced(); debounced(); debounced();
  t.mock.timers.tick(299);
  assert.equal(save.mock.callCount(), 0);
  t.mock.timers.tick(1);
  assert.equal(save.mock.callCount(), 1);
});
Run with node --test mock.test.mjs (passes on Node.js 26, 24 and 22)
✔ a debounced save runs once, 300 ms after the last call
ℹ tests 1
ℹ pass 1
ℹ fail 0

What changed recently

Table 3. Timer-related changes by version, from the Node.js API documentation
Version Change

v24.2.0

timeout[Symbol.dispose]() and immediate[Symbol.dispose]() are no longer experimental. They are still marked Experimental in the Node.js 22 documentation.

v24.0.0

The long-deprecated internal helpers timers.enroll() (DEP0095), timers.unenroll() (DEP0096), timers.active() (DEP0126) and timers._unrefActive() (DEP0127) reached End-of-Life and were removed. Use setTimeout(), clearTimeout() and timeout.refresh() instead; the codemod @nodejs/timers-deprecations migrates old code.

v23.1.0

The test runner’s mock timers (MockTimers, added in v20.4.0 and v18.19.0) became stable.

v20.5.0 and v18.18.0

[Symbol.dispose]() added to Timeout and Immediate.

v18.0.0

An invalid callback throws ERR_INVALID_ARG_TYPE instead of ERR_INVALID_CALLBACK.

v17.3.0 and v16.14.0

timersPromises.scheduler.wait() and scheduler.yield() added (still Experimental).

v16.0.0

node:timers/promises graduated from experimental (added in v15.0.0; setInterval() in v15.9.0).

v14.9.0 and v12.19.0

Timeout gained Symbol.toPrimitive, so clearTimeout() accepts the numeric id.

v11.0.0

hasRef() added to Timeout and Immediate.

The documentation lists no other behavioural change to the timer functions on Node.js 26; the API is stable and slow-moving, and the differences between the three lines covered here are the ones in the table above plus the NaN and using notes in the sections before.

Where the books are outdated

Two O’Reilly books from 2025 touch on timers: Efficient Node.js by Samer Buna and Node.js Projects by Jonathan Wexler. Both are useful for the core idea, and both mostly use the callback forms. What they do not cover, or cover only briefly, is summarised here against the current documentation.

Table 4. Gaps between the books and the current Node.js documentation
Topic Current state

node:timers/promises

Mentioned briefly in Efficient Node.js (a promise-based setTimeout); the async-iterable setInterval(), setImmediate(), ref and signal options and scheduler.wait()/scheduler.yield() are the modern way to await delays and are not part of the walkthroughs. Examples built on util.promisify(setTimeout) are legacy: the promise variant is built in.

Polling with setInterval()

Node.js Projects polls feeds with setInterval(). As shown in setTimeout, setInterval and setImmediate, a slow callback can overlap with the next run, and an uncleared interval keeps the process alive; use the async-iterable interval or a recursive timer, add an AbortSignal, and unref() background timers that must not block shutdown.

ref(), unref(), hasRef(), refresh()

The books use timers as simple delays. They do not use unref() or refresh(), which are the tools for shutdown behaviour and idle timeouts (unref() is used in Efficient Node.js for a child process, not for a timer).

using and Symbol.dispose

Newer than both books' examples: stable on timers since Node.js 24.2.0.

Accuracy and limits

Neither book discusses the 1 ms minimum, the 2147483647 ms ceiling and its TimeoutOverflowWarning, or the fact that a blocked loop delays all timers.

Testing

Efficient Node.js shows mock.timers, which was stable from v23.1.0; t.mock.timers auto-restores at the end of a test, and tick() can also drive Date.

Removed internals

Older tutorials may use timers.enroll(), timers.unenroll(), timers.active() or timers._unrefActive(). They were removed in Node.js 24.0.0.

References