Skip to content

Mocking builtins does not affect already-imported ESM namespace objects #62081

Description

@fenying

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

Activity

  1. Siddhartha-singh01 commented on Mar 6, 2026

    @Siddhartha-singh01

    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!

  2. Renegade334 commented on Mar 7, 2026

    @Renegade334
    Member

    The following change should get this working for you:

    -import * as NodeTimers from 'node:timers/promises';
    +import NodeTimers from 'node:timers/promises';

    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 call syncBuiltinESMExports() if setting up mocks that replace builtins.

  3. changed the title [-]node:timer/promises does not work with mock.timers.runAll() in ESM[/-] [+]Mocking builtins does not affect already-imported ESM namespace objects[/+] on Mar 8, 2026
  4. added
    esmIssues and PRs related to the ECMAScript Modules implementation.
    test_runnerIssues and PRs related to the test runner subsystem.
    on Mar 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    esmIssues and PRs related to the ECMAScript Modules implementation.test_runnerIssues and PRs related to the test runner subsystem.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions