jsguides

timers

setTimeout(callback[, delay[, ...args]])

The node:timers module schedules callbacks to run after a delay, on a recurring interval, or as soon as the event loop is idle. You almost never import it directly: the six scheduling functions are exposed as globals, so setTimeout, setInterval, and setImmediate work in any file with no require. The module object itself is worth importing only when you need the Timeout and Immediate classes, the refresh() method, or the timers/promises sub-module.

import { setTimeout, setImmediate } from "node:timers";     // named imports
import { setTimeout as sleep } from "node:timers/promises"; // promise-based

Scheduling timers

The three scheduling functions return a Timeout (or Immediate) object that you pass to the matching clear* function to cancel.

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

Runs callback once after delay milliseconds. The default delay is 1; values below 1, above 2147483647, or NaN are clamped to 1. Extra arguments are forwarded to the callback.

setTimeout((name, count) => {
  console.log(`${name} ran ${count} times`);
}, 100, "tick", 1);
// tick ran 1 times

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

Runs callback every delay milliseconds until cancelled. Returns a Timeout (the function name is historical, not a different object). The interval keeps firing until you call clearInterval on the returned handle, or the process exits with no other work pending.

let n = 0;
const id = setInterval(() => {
  console.log(++n);
  if (n === 3) clearInterval(id);
}, 50);
// 1
// 2
// 3

setImmediate(callback[, …args])

Queues callback to run in the “check” phase of the event loop, after I/O for the current iteration completes. Inside an I/O callback, setImmediate always fires before setTimeout(..., 0).

import { readFile } from "node:fs/promises";

await readFile(__filename);
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
// immediate
// timeout

Cancelling timers

clearTimeout, clearInterval, and clearImmediate each take the value returned by their scheduling function and return undefined. clearTimeout and clearInterval are interchangeable on the same Timeout object.

const t = setTimeout(() => console.log("never"), 1000);
clearTimeout(t);   // ok
clearInterval(t);  // also ok — same Timeout object

A Timeout can also be cancelled implicitly when it falls out of scope, but explicit clear* calls are clearer and let you cancel from anywhere with a reference.

The timer classes

The Timeout and Immediate objects returned by the scheduling functions are not opaque handles. They expose a small API for managing the timer’s effect on the event loop.

ref() and unref()

By default, a scheduled timer keeps the event loop alive. unref() removes that hold so the process can exit even if the timer is still pending. Both methods are idempotent.

const beat = setInterval(() => console.log("heartbeat"), 1000);
beat.unref();
// heartbeat
// (process can now exit before the next beat)

Call ref() to put the timer back into the active set. hasRef() reports whether it currently does.

refresh()

Resets a timer’s start time to now, including timers whose callback has already run. Useful for adaptive backoff and “leading-edge debounce” patterns.

const t = setTimeout(() => console.log("fire!"), 1000);
t.refresh();
// Timer is now scheduled to fire 1000ms from *this* call.

Symbol.dispose (using syntax)

Timeout and Immediate instances implement Symbol.dispose, which calls the matching clear* function when the declaring block exits. This makes timers a natural fit for using declarations, which guarantee cleanup on scope exit, exceptions, or early returns.

{
  using t = setTimeout(() => console.log("fired"), 1000);
  t.unref();
  // block exit cancels the timer
}

Symbol.dispose on timers became stable in Node v24.2.0 (v20.5.0 / v18.18.0 added it as experimental). The classic alternative on older versions is try { ... } finally { clearTimeout(t); }.

The node:timers/promises sub-module

Added in v15.0.0, this sub-module gives you promise-returning versions of the scheduling functions. It is the right answer for any await-based code path, including top-level await in ES modules.

import {
  setTimeout as sleep,
  setInterval as every,
  scheduler,
} from "node:timers/promises";

await sleep(250); // pause 250ms without blocking the event loop

Cancelling with an AbortSignal

Every function in this sub-module accepts an options object with ref and signal. Aborting the signal rejects the promise with an AbortError (for setInterval, iteration simply stops).

import { setTimeout as sleep } from "node:timers/promises";

const ac = new AbortController();
setTimeout(() => ac.abort(), 50);

try {
  await sleep(1000, "never reached", { signal: ac.signal });
} catch (err) {
  console.error(err.name); // "AbortError"
}

setInterval as an async iterator

timersPromises.setInterval returns an AsyncIterableIterator that yields on each tick. Break out of the loop to stop it. Each yielded value is whatever you passed as the second argument (here, Date.now()), which is a clean way to capture the tick timestamp without an extra Date.now() call inside the loop body.

import { setInterval as every } from "node:timers/promises";

for await (const start of every(100, Date.now())) {
  console.log(start);
  if (Date.now() - start > 500) break;
}

scheduler.wait and scheduler.yield

scheduler.wait(delay) is equivalent to setTimeout(delay, undefined, options) and exists for spec-alignment with the WICG Scheduling APIs draft. scheduler.yield() pauses until the event loop can resume, which is useful for breaking up long synchronous work without giving the process away.

import { scheduler } from "node:timers/promises";

await scheduler.wait(0);     // yield to the event loop
await scheduler.yield();     // same idea, no fixed delay

Both are still flagged experimental in current Node releases and may change before stabilizing.

Common pitfalls

Delay clamping means setTimeout(fn, 0) is “next tick,” not “now.” Values below 1, above 2_147_483_647, or NaN are silently clamped to 1 ms.

setTimeout(() => console.log("a"), 0);
setTimeout(() => console.log("b"), -5);
setTimeout(() => console.log("c"), NaN);
// All three fire after roughly 1ms, in registration order: a, b, c

No timing guarantees. Node schedules timers “as close as possible” to the requested time but provides no precision or ordering guarantee across timers. Use the Performance API or a monotonic clock for measurement.

process.nextTick is not setImmediate. nextTick runs before the current I/O phase completes, while setImmediate runs after I/O. setTimeout(fn, 0) sits somewhere in between and is not a substitute for either.

clearInterval and clearTimeout are aliases. They do the same thing to a Timeout. Pick one per call site for readability; the runtime does not care.

unref() is not stackable. Calling it ten times does not unref by ten. ref() puts the timer back in the active set; hasRef() tells you which state it is in.

Reusing an expired timer with refresh(). If a setTimeout callback has already fired, calling refresh() schedules it again with a fresh delay. Convenient for debounce, easy to misuse for “did this fire?” tracking.

Browser vs Node return values. Browsers return a numeric id; Node returns a Timeout object. Timeout has Symbol.toPrimitive so cross-runtime code that does clearTimeout(id) still works, but treat the value opaquely when writing library code.

See Also