Skip to content

[Bug]: Concurrent ESM mock imports duplicate instances and stale factories overwrite reset cache #16539

Open

Description

@shoutoutuoadi325

Version

30.5.2. Also reproduced on main at 7261444.

Steps to reproduce

Minimal reproduction: https://github.com/shoutoutuoadi325/jest-esm-async-mock-repro

  1. Clone the reproduction repository.
  2. Run npm ci.
  3. Run npm test.

The reproduction uses native ESM, transform: {} and --experimental-vm-modules. It contains two tests:

  • Register an asynchronous jest.unstable_mockModule factory and concurrently import its target with Promise.all.
  • Start a factory, call jest.resetModules(), import the new mock, then let the old factory finish.

Expected behavior

Concurrent imports should invoke the factory once and return the same module namespace and exported object.

An old factory should complete for its original caller without replacing the newer mock cached after resetModules().

Actual behavior

Both tests fail on 30.5.2:

concurrent imports share one mock factory result
  Expected number of calls: 1
  Received number of calls: 2

an old factory does not overwrite a newer mock after resetModules
  Expected: 2
  Received: 1

The concurrent imports produce distinct namespaces and exported object identities. When the older factory finishes last, subsequent imports receive the old generation.

Additional context

In main's EsmLoader.importMock, the cache is checked before await factory(), but the result is registered only after the factory completes. Concurrent callers therefore all see a cache miss. The eventual write also uses the registry active at completion, which can belong to a newer reset or isolation scope.

The same lifecycle needs completion handling in importMockSync: it caches an asynchronous factory's Promise, but never promotes the fulfilled entry to its evaluated module. On Node 24, a completed asynchronous import can therefore still make a subsequent require() report ERR_REQUIRE_ASYNC_MODULE.

I have a fix that caches the pending operation, retains its original registry, and updates or removes the entry only while it still belongs to that operation. Regression tests cover concurrent sync/async factories, shared rejection and retry, reset completion ordering, isolation and synchronous reuse after fulfillment.

Environment

System:
  OS: macOS 27.2
  CPU: (12) arm64 Apple M4 Pro
Binaries:
  Node: 22.23.4 (also tested main with Node 24.22.1)
  npm: 10.9.9
npmPackages:
  jest: 30.5.2 => 30.5.2

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions