Repository navigation
Mocking builtins does not affect already-imported ESM namespace objects #62081
Copy link
Copy link
Closed
nodejs/nodejs.org
#8707Labels
esmIssues and PRs related to the ECMAScript Modules implementation.Issues and PRs related to the ECMAScript Modules implementation.test_runnerIssues and PRs related to the test runner subsystem.Issues and PRs related to the test runner subsystem.
Description
Version
v24.14.0
Platform
Linux xxxxxxx 6.18.13-arch1-1 #1 SMP PREEMPT_DYNAMIC Wed, 25 Feb 2026 23:12:35 +0000 x86_64 GNU/Linux
Subsystem
No response
What steps will reproduce the bug?
Here is the reproduce code:
// timer-failed.test.mjs
import * as NodeTest from 'node:test';
import * as NodeTimers from 'node:timers/promises';
import * as NodeAssert from 'node:assert';
NodeTest.describe('Test suite for the package', async () => {
await NodeTest.it('should pass this test', async (ctx) => {
ctx.mock.timers.enable({ apis: ['Date', 'setTimeout'], now: 12345 });
const startTime = Date.now();
let t1 = 0;
await Promise.all([
(async () => {
await NodeTimers.setTimeout(1000, null);
t1 = Date.now();
})(),
(async () => {
ctx.mock.timers.runAll();
})()
]);
console.log(startTime);
console.log(t1);
NodeAssert.strictEqual(t1 - startTime, 1000);
});
});You can see the 2 console.log will print 12345\n12345, which is incorrect.
If you rewrite the file to CJS module, it works perfectly.
// timer.test.cjs
const NodeTest = require('node:test');
const NodeTimers = require('node:timers/promises');
const NodeAssert = require('node:assert');
NodeTest.describe('Test suite for the package', async () => {
await NodeTest.it('should pass this test', async (ctx) => {
ctx.mock.timers.enable({ apis: ['Date', 'setTimeout'], now: 12345 });
const startTime = Date.now();
let t1 = 0;
await Promise.all([
(async () => {
await NodeTimers.setTimeout(1000, null);
t1 = Date.now();
})(),
(async () => {
ctx.mock.timers.runAll();
})()
]);
console.log(startTime);
console.log(t1);
NodeAssert.strictEqual(t1 - startTime, 1000);
});
});Or just replace the timer code with await new Promise(resolve => setTimeout(resolve, 1000));, it also works well.
How often does it reproduce? Is there a required condition?
Everytime I run this code
What is the expected behavior? Why is that the expected behavior?
The code should print 12345\n13345, and pass the test.
In ESM, the node:timer/promises with ctx.mock.timers.runAll() should work as same as it does in CJS.
What do you see instead?
12345
12345
▶ Test suite for the package
✖ should pass this test (1002.976086ms)
✖ Test suite for the package (1004.29576ms)
Additional information
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
esmIssues and PRs related to the ECMAScript Modules implementation.Issues and PRs related to the ECMAScript Modules implementation.test_runnerIssues and PRs related to the test runner subsystem.Issues and PRs related to the test runner subsystem.
Activity
Siddhartha-singh01 commentedon Mar 6, 2026
I’ve been investigating this issue over the last couple of days and have identified the root cause. It stems from the fact that ESM exports are immutable bindings, so even when mock.timers replaces the internal CommonJS exports, existing ESM imports (like import { setTimeout } from 'node:timers/promises') still reference the original local functions.
I’ve implemented a fix that triggers module.syncBuiltinESMExports() whenever the mocking state changes, which correctly propagates the mocked timers to ESM modules.
I'm opening a Pull Request with this fix now!
Renegade334 commentedon Mar 7, 2026
The following change should get this working for you:
The former imports the ESM module namespace object, whereas the latter imports the module's exports object. There's no need to run
syncBuiltinESMExports()with the latter pattern.The documentation for mocks should be more explicit about the fact that users will either need to use the default export, use dynamic
import(), or callsyncBuiltinESMExports()if setting up mocks that replace builtins.test_runner: sync built-in ESM exports for mock timers
[-]node:timer/promises does not work with mock.timers.runAll() in ESM[/-][+]Mocking builtins does not affect already-imported ESM namespace objects[/+]