From b008bbe18ec85631a072a5eb131f248458cefae1 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 20 Sep 2026 09:45:54 +0000 Subject: [PATCH] timers: allow setTimeout to accept a delay of 0 A setTimeout() delay of 0 (or a positive sub-millisecond delay, which is truncated to 0 by insert()) is now scheduled as soon as possible instead of being clamped to 1 ms, matching browser behavior. Negative delays, NaN, and values above TIMEOUT_MAX are still clamped to 1 ms, and setInterval() keeps clamping delays below 1 ms to 1 ms to avoid firing as fast as the event loop allows. Refs: https://github.com/nodejs/node/issues/46596 PR-URL: https://github.com/nodejs/node/pull/66155 Assisted-by: pi Signed-off-by: Matteo Collina --- doc/api/timers.md | 8 ++++-- lib/internal/timers.js | 12 ++++++++- .../test-timers-zero-delay-ordering.js | 25 +++++++++++++++++++ test/parallel/test-timers.js | 4 +-- 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 test/parallel/test-timers-zero-delay-ordering.js diff --git a/doc/api/timers.md b/doc/api/timers.md index cdbed03ba7a..e2dec0b7dfe 100644 --- a/doc/api/timers.md +++ b/doc/api/timers.md @@ -276,8 +276,12 @@ Node.js makes no guarantees about the exact timing of when callbacks will fire, nor of their ordering. The callback will be called as close as possible to the time specified. -When `delay` is larger than `2147483647` or less than `1` or `NaN`, the `delay` -will be set to `1`. Non-integer delays are truncated to an integer. +When `delay` is larger than `2147483647`, a negative number, or `NaN`, the +`delay` will be set to `1`. A delay of `0` (or a positive sub-millisecond +value, which is truncated to `0`) schedules the callback for the timers phase +of a subsequent event loop turn, without the `1` ms minimum delay. It does not +run within the current turn; the callback is invoked during a later iteration +of the event loop. Non-integer delays are truncated to an integer. If `callback` is not a function, a [`TypeError`][] will be thrown. diff --git a/lib/internal/timers.js b/lib/internal/timers.js index 9904cb7cb2e..200257fb527 100644 --- a/lib/internal/timers.js +++ b/lib/internal/timers.js @@ -239,7 +239,17 @@ class Timeout { '\nTimeout duration was set to 1.', 'TimeoutNaNWarning'); } - after = 1; // Schedule on next tick, follows browser behavior + + // setTimeout() accepts a delay of 0 or a positive sub-millisecond + // delay, which is truncated to 0 by insert() and thus scheduled for a + // subsequent timers phase of the event loop without the 1 ms minimum, + // matching browsers (it still does not run within the current turn). + // Every other invalid delay (and every setInterval() delay below 1 ms, + // so it does not fire as fast as the event loop allows) is still + // clamped to 1 ms. + if (isRepeat || after < 0 || NumberIsNaN(after) || after > TIMEOUT_MAX) { + after = 1; // Schedule on next tick, follows browser behavior + } } this._idleTimeout = after; diff --git a/test/parallel/test-timers-zero-delay-ordering.js b/test/parallel/test-timers-zero-delay-ordering.js new file mode 100644 index 00000000000..a1f1922b079 --- /dev/null +++ b/test/parallel/test-timers-zero-delay-ordering.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// setTimeout with a delay of 0 should schedule the callback as soon as +// possible, so that it runs before a timer scheduled with a 1 ms delay. +// See https://github.com/nodejs/node/issues/46596 + +const order = []; + +setTimeout(common.mustCall(() => order.push('one')), 1); +setTimeout(common.mustCall(() => order.push('zero')), 0); + +setTimeout(common.mustCall(() => { + assert.deepStrictEqual(order, ['zero', 'one']); +}), 2); + +// A zero-millisecond delay must still be allowed for the promisified variant. +let resolved; +const p = require('node:timers/promises').setTimeout(0); +p.then(common.mustCall(() => { resolved = true; })); + +setTimeout(common.mustCall(() => { + assert.strictEqual(resolved, true); +}), 2); diff --git a/test/parallel/test-timers.js b/test/parallel/test-timers.js index 11c6e106e85..c6380136a2f 100644 --- a/test/parallel/test-timers.js +++ b/test/parallel/test-timers.js @@ -65,8 +65,8 @@ inputs.forEach((value, index) => { }, value); }); -// All values in inputs array coerce to 1 ms. Therefore, they should all run -// before a timer set here for 2 ms. +// All values in inputs array coerce to a short delay (0 ms or 1 ms). +// Therefore, they should all run before a timer set here for 2 ms. setTimeout(common.mustCall(() => { // Assert that all other timers have run