setTimeout, setInterval and setImmediate are globals, but Node's return a Timeout object rather than a number, with methods the browser has no equivalent for. unref() tells libuv not to count the timer when deciding whether the process still has work; refresh() restarts the countdown from now, which is how idle-connection timeouts avoid allocating a timer per request.
import { setTimeout as delay, setInterval as every } from 'node:timers/promises';
const t0 = performance.now();
const ms = () => String(Math.round(performance.now() - t0)).padStart(4);
setTimeout(() => console.log(ms(), 'timers phase'), 0);
setImmediate(() => console.log(ms(), 'check phase'));
setInterval(() => {}, 1000).unref(); // a heartbeat that never holds the process open
let n = 0;
for await (const _ of every(20)) { // the body finishes before the next tick fires
console.log(ms(), 'poll', ++n);
await delay(15);
if (n === 3) break; // break clears the interval for you
}
console.log(ms(), 'exited');0 check phase 3 timers phase 32 poll 1 66 poll 2 98 poll 3 116 exited
The delay you ask for is a minimum. setTimeout(fn, 0) is clamped to 1 ms and then waits for the timers phase, so from the main module its order against setImmediate depends on how long startup took — run this twice and the first two lines may swap. The polls land 35 ms apart, not 20, because timers/promises schedules the next tick only after the body returns; a callback setInterval fires on a fixed grid and lets slow bodies pile up.
node:timers/promises is stable and its setTimeout, setImmediate and setInterval accept { signal, ref }; the same module's scheduler.wait() and scheduler.yield() remain Stability 1 - Experimental.