Skip to content

v11.3.0 top-level await in ESM build breaks jsdom/vitest (ERR_REQUIRE_ASYNC_MODULE) #397

Description

@esetnik

Summary

lru-cache@11.3.0 (PR #395) introduced a top-level await in the ESM build via src/diagnostics-channel-esm.mts:

export const [metrics, tracing] = await import('node:diagnostics_channel').then(...).catch(...)

This breaks any CJS module that transitively loads the ESM build of lru-cache, most notably jsdom via @asamuzakjp/css-color (which is pure ESM with no CJS entrypoint).

Dependency chain

jsdom (CJS)
  → @asamuzakjp/css-color (pure ESM, "type": "module")
    → lru-cache@11.3.0 (ESM build now has TLA)
      → ERR_REQUIRE_ASYNC_MODULE

Error

Error: require() cannot be used on an ESM graph with top-level await.
Use import() instead. To see where the top-level await comes from,
use --experimental-print-required-tla.
  From jsdom/lib/jsdom/living/css/helpers/css-values.js
  Requiring @asamuzakjp/css-color/dist/esm/index.js

This causes every Vitest test using jsdom to fail — no tests run at all.

Impact

This is affecting projects using Vitest + jsdom on Node 22+. The issue surfaced broadly today via Renovate/Dependabot lock file maintenance PRs that re-resolved lru-cache@^11.x to 11.3.0.

Related jsdom issues filed today:

Also related: #396 (Angular test runner memory leak from the same diagnostics-channel feature).

Why this happens

The exports map correctly routes require → CJS and import → ESM. However, when a CJS package (jsdom) requires a pure-ESM package (@asamuzakjp/css-color), Node resolves the ESM graph. Within that ESM graph, lru-cache's ESM entrypoint is loaded — which now contains TLA, making the entire graph impossible to require() synchronously.

Suggested fix

The comment in the source notes: "the first tick of metrics will be missed but that probably doesn't matter much." This suggests a non-blocking approach would be acceptable:

// Instead of TLA:
// export const [metrics, tracing] = await import('node:diagnostics_channel')...

// Use non-blocking init:
let metrics = dummyMetrics, tracing = dummyTracing;
import('node:diagnostics_channel').then(dc => {
  metrics = dc.channel('lru-cache:metrics');
  tracing = dc.tracingChannel('lru-cache:tracing');
}).catch(() => {});
export { metrics, tracing };

This preserves the non-Node fallback behavior without introducing TLA.

Workaround

Pin lru-cache to 11.2.7:

"pnpm": {
  "overrides": {
    "lru-cache": "11.2.7"
  }
}

Versions

  • lru-cache: 11.3.0
  • jsdom: 29.0.1
  • @asamuzakjp/css-color: 5.0.1–5.1.5 (all affected)
  • Node: 22+
  • Vitest: 4.1.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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      Sponsor
      SponsoredKunjungi sekarang
      Promo