Skip to content

Commit b008bbe

Browse files
committed
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: #46596 PR-URL: #66155 Assisted-by: pi Signed-off-by: Matteo Collina <matteo.collina@gmail.com>
1 parent ebef774 commit b008bbe

4 files changed

Lines changed: 44 additions & 5 deletions

File tree

‎doc/api/timers.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,12 @@ Node.js makes no guarantees about the exact timing of when callbacks will fire,
276276
nor of their ordering. The callback will be called as close as possible to the
277277
time specified.
278278

279-
When `delay` is larger than `2147483647` or less than `1` or `NaN`, the `delay`
280-
will be set to `1`. Non-integer delays are truncated to an integer.
279+
When `delay` is larger than `2147483647`, a negative number, or `NaN`, the
280+
`delay` will be set to `1`. A delay of `0` (or a positive sub-millisecond
281+
value, which is truncated to `0`) schedules the callback for the timers phase
282+
of a subsequent event loop turn, without the `1` ms minimum delay. It does not
283+
run within the current turn; the callback is invoked during a later iteration
284+
of the event loop. Non-integer delays are truncated to an integer.
281285

282286
If `callback` is not a function, a [`TypeError`][] will be thrown.
283287

‎lib/internal/timers.js‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,17 @@ class Timeout {
239239
'\nTimeout duration was set to 1.',
240240
'TimeoutNaNWarning');
241241
}
242-
after = 1; // Schedule on next tick, follows browser behavior
242+
243+
// setTimeout() accepts a delay of 0 or a positive sub-millisecond
244+
// delay, which is truncated to 0 by insert() and thus scheduled for a
245+
// subsequent timers phase of the event loop without the 1 ms minimum,
246+
// matching browsers (it still does not run within the current turn).
247+
// Every other invalid delay (and every setInterval() delay below 1 ms,
248+
// so it does not fire as fast as the event loop allows) is still
249+
// clamped to 1 ms.
250+
if (isRepeat || after < 0 || NumberIsNaN(after) || after > TIMEOUT_MAX) {
251+
after = 1; // Schedule on next tick, follows browser behavior
252+
}
243253
}
244254

245255
this._idleTimeout = after;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
5+
// setTimeout with a delay of 0 should schedule the callback as soon as
6+
// possible, so that it runs before a timer scheduled with a 1 ms delay.
7+
// See https://github.com/nodejs/node/issues/46596
8+
9+
const order = [];
10+
11+
setTimeout(common.mustCall(() => order.push('one')), 1);
12+
setTimeout(common.mustCall(() => order.push('zero')), 0);
13+
14+
setTimeout(common.mustCall(() => {
15+
assert.deepStrictEqual(order, ['zero', 'one']);
16+
}), 2);
17+
18+
// A zero-millisecond delay must still be allowed for the promisified variant.
19+
let resolved;
20+
const p = require('node:timers/promises').setTimeout(0);
21+
p.then(common.mustCall(() => { resolved = true; }));
22+
23+
setTimeout(common.mustCall(() => {
24+
assert.strictEqual(resolved, true);
25+
}), 2);

‎test/parallel/test-timers.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ inputs.forEach((value, index) => {
6565
}, value);
6666
});
6767

68-
// All values in inputs array coerce to 1 ms. Therefore, they should all run
69-
// before a timer set here for 2 ms.
68+
// All values in inputs array coerce to a short delay (0 ms or 1 ms).
69+
// Therefore, they should all run before a timer set here for 2 ms.
7070

7171
setTimeout(common.mustCall(() => {
7272
// Assert that all other timers have run

0 commit comments

Comments
 (0)