Node.js Error

ERR_REQUIRE_ASYNC_MODULE

Complete reference for Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead. — what it means, every cause, and all fixes including the --experimental-print-required-tla diagnostic flag.

Quick Answer: Node.js throws ERR_REQUIRE_ASYNC_MODULE when require() is called on an ES module (or any module in its dependency graph) that uses top-level await. The fastest fix is replacing require('pkg') with const mod = await import('pkg'). To find which file has the top-level await, run with --experimental-print-required-tla. This error first appears in Node.js 20.19+ and 22.12+ (when require(esm) was enabled by default).
Exact error strings you will see in Node.js:

What is ERR_REQUIRE_ASYNC_MODULE?

ERR_REQUIRE_ASYNC_MODULE is thrown by the Node.js module loader when require() is called on an ES module whose evaluation is asynchronous — meaning the module itself, or any module in its import chain, contains a top-level await expression.

require() is fundamentally synchronous: it blocks the event loop until the module is fully evaluated and returns its exports. Top-level await makes a module asynchronous — it cannot complete synchronously — so Node.js refuses to load it via require() and throws this error instead.

This error was introduced alongside the require(ESM) feature that became the default in Node.js 20.19.0 and Node.js 22.12.0. Before those versions, any require() of an ESM file threw ERR_REQUIRE_ESM. Starting with those versions, synchronous ESM can be require()d successfully — but asynchronous ESM (containing top-level await) throws the new ERR_REQUIRE_ASYNC_MODULE error.

Full Error Example

$ node app.js
node:internal/modules/cjs/loader:1431
    throw new ERR_REQUIRE_ASYNC_MODULE(mod.filename);
          ^

Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph
with top-level await. Use import() instead. To find the top-level await,
use --experimental-print-required-tla.
    at Object.<anonymous> (/project/app.js:3:17)
    at Module._compile (node:internal/modules/cjs/loader:1364:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1422:10)
    at Module.load (node:internal/modules/cjs/loader:1203:32)
    at Module._load (node:internal/modules/cjs/loader:1019:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/run_main:128:12)
    at node:internal/main/run_main_module:28:49 {
  code: 'ERR_REQUIRE_ASYNC_MODULE'
}

With --experimental-print-required-tla diagnostic

$ node --experimental-print-required-tla app.js
(node:12345) ExperimentalWarning: --experimental-print-required-tla is an experimental feature.
(Use `node --trace-warnings ...` to show where the warning was created)

  Top-level await in /project/node_modules/some-package/dist/index.mjs line 3:

    const [metrics, tracing] = await import('node:diagnostics_channel')
                               ^^^^^

Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph
with top-level await. Use import() instead.
    ...

Error Object Properties

PropertyValueNotes
err.code'ERR_REQUIRE_ASYNC_MODULE'Use this to catch specifically in error handlers
err.messagerequire() cannot be used on an ESM graph with top-level await. Use import() instead.Exact string; verbatim in stack traces
err.name'Error'Inherits from Error

Node.js Version Matrix

Node.js versionrequire(sync ESM)require(async ESM with TLA)
≤ 20.18.xThrows ERR_REQUIRE_ESMThrows ERR_REQUIRE_ESM
20.19.0+Works — returns module exportsThrows ERR_REQUIRE_ASYNC_MODULE
≤ 22.11.xThrows ERR_REQUIRE_ESMThrows ERR_REQUIRE_ESM
22.12.0+Works — returns module exportsThrows ERR_REQUIRE_ASYNC_MODULE
23.x, 24.x, 26.xWorks — returns module exportsThrows ERR_REQUIRE_ASYNC_MODULE
Upgrading from Node 20.18 to 20.19 (or 22.11 to 22.12) can surface this error. Code that previously failed with ERR_REQUIRE_ESM will now succeed if the ESM is synchronous, but will throw the new ERR_REQUIRE_ASYNC_MODULE if the ESM contains top-level await. Tools that catch ERR_REQUIRE_ESM by error code (e.g. Serverless Framework 3.40.0) will silently miss the new error code.

All Causes at a Glance

Cause What triggers it Recommended fix
Direct require() of a TLA module require('./async-module.mjs') where the file has await at the top level Use await import('./async-module.mjs')
Transitive TLA in dependency graph You require('pkg'); pkg imports dep; dep has top-level await Replace require() with import(), or pin/patch the dep
Node.js upgrade (20.18→20.19 or 22.11→22.12) Previously hidden by ERR_REQUIRE_ESM; now surfaces after require(esm) was enabled by default Use import() or remove TLA from the dependency
Package introduced TLA in a minor/patch release lru-cache@11.3.0 added TLA via diagnostics-channel-esm.mts Pin to version before TLA (e.g. lru-cache@11.2.7) or update to patched release
Dynamic import() result re-exported with TLA ESM module does export const x = await dynamicImport() at top level Remove the top-level await; lazy-load inside a function instead
Tooling catches ERR_REQUIRE_ESM but not ERR_REQUIRE_ASYNC_MODULE Serverless Framework, firebase-tools, or custom loaders hardcode the old error code string Update the error code check or upgrade the tool to a version that handles both codes
TypeScript compiled output calls require() on TLA ESM TypeScript with "module": "commonjs" emits require() for dynamic imports that resolve to async ESM Use "module": "nodenext" or "module": "es2022" in tsconfig

Cause 1 – Direct require() of a file with top-level await

The simplest case: you are directly calling require() on a .mjs file or an ESM package that contains a top-level await expression.

// async-config.mjs  ← contains top-level await
const config = await fetch('https://api.example.com/config').then(r => r.json());
export default config;

// app.js (CommonJS)  ← this will throw
const config = require('./async-config.mjs');
// Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph
// with top-level await. Use import() instead.

Cause 2 – Top-level await hidden in a transitive dependency

You never call require() on an async module directly — but a package you require imports another module that uses top-level await somewhere in its graph. The error still propagates to your call site.

// your-app.js (CJS)
const jsdom = require('jsdom');   // requires @asamuzakjp/css-color
                                  // which requires lru-cache@11.3.0
                                  // which has TLA in diagnostics-channel-esm.mts
// → Error [ERR_REQUIRE_ASYNC_MODULE]

// Dependency chain:
// jsdom (CJS)
//   └── @asamuzakjp/css-color (ESM)
//         └── lru-cache@11.3.0 (ESM with top-level await)  ← the culprit

Use --experimental-print-required-tla to pin down the exact file:

node --experimental-print-required-tla your-app.js
# Prints: Top-level await in /project/node_modules/lru-cache/dist/esm/index.js line 12

Cause 3 – Node.js upgrade surface (20.18 → 20.19, 22.11 → 22.12)

Before Node.js 20.19 and 22.12, all require() calls on ESM files threw ERR_REQUIRE_ESM — including ones that would have worked if the ESM were synchronous. After those releases, require(esm) is enabled by default: synchronous ESM loads correctly, but async ESM (TLA) now surfaces as ERR_REQUIRE_ASYNC_MODULE.

// Node.js 20.18 behavior:
require('./sync-esm.mjs')   // throws ERR_REQUIRE_ESM
require('./async-esm.mjs')  // throws ERR_REQUIRE_ESM

// Node.js 20.19+ behavior:
require('./sync-esm.mjs')   // works! returns module.exports
require('./async-esm.mjs')  // throws ERR_REQUIRE_ASYNC_MODULE  ← new error

Cause 4 – Package silently added top-level await in a patch release

A package you depend on introduced top-level await in a minor or patch version, breaking any CJS consumer that loads it. lru-cache v11.3.0 is the canonical real-world example: it added optional diagnostics via diagnostics-channel-esm.mts which contained:

// lru-cache/src/diagnostics-channel-esm.mts (v11.3.0, introduced TLA)
export const [metrics, tracing] = await import('node:diagnostics_channel')
  .then(dc => [dc.channel('lru-cache:set'), dc.channel('lru-cache:get')]);
  //   ^^^^^ top-level await — blocks synchronous require()

This was fixed in lru-cache v11.3.1 by replacing the top-level await with a .then()-based initializer. If you cannot upgrade, pin to v11.2.7 or earlier.

Fix 1 – Replace require() with dynamic import() (recommended)

The universal fix that works in any CommonJS file with no project-wide migration. Dynamic import() returns a Promise and handles async module graphs natively.

// Before (throws ERR_REQUIRE_ASYNC_MODULE)
const mod = require('./async-module.mjs');

// After — using top-level await (requires Node.js 14.8+ in ESM, or CJS with async IIFE)
const mod = await import('./async-module.mjs');
const value = mod.default;  // access default export
const { named } = mod;      // access named exports

// If you are in a synchronous CJS context without top-level await support:
(async () => {
  const { default: config } = await import('./async-config.mjs');
  console.log(config);
})();

// For a module that needs to be loaded once and reused:
let _pkg;
async function getPkg() {
  if (!_pkg) _pkg = await import('some-async-esm-package');
  return _pkg;
}
Default vs named exports: ESM packages may use export default or named exports. Dynamic import() always returns a module namespace object. Access the default export as mod.default or destructure as const { default: myName } = await import('...'). Named exports: const { readFile, writeFile } = await import('fs/promises').

Fix 2 – Remove top-level await from the module

If the top-level await is in code you control, refactor it out. The module then evaluates synchronously and can be safely require()d.

// Before — module with top-level await (cannot be require()'d)
// async-config.mjs
const config = await loadConfig();
export default config;

// After option A — lazy initialization inside a function
// config.mjs
let _config;
export async function getConfig() {
  if (!_config) _config = await loadConfig();
  return _config;
}

// After option B — replace TLA with .then() for side effects
// diagnostics.mjs
let metrics, tracing;
import('node:diagnostics_channel').then(dc => {
  metrics = dc.channel('app:metric');
  tracing = dc.channel('app:trace');
});
export { metrics, tracing };

Fix 3 – Convert the caller to ESM

ESM callers can import async ESM modules using static import or dynamic import(). Converting the top-level caller to ESM means the entire module graph loads asynchronously, eliminating the conflict.

// 1. Add to package.json:
{ "type": "module" }

// 2. Rename .js files to .mjs, or rely on "type": "module"

// 3. Replace require() with static import:
// Before (CJS):
const config = require('./async-config.mjs');

// After (ESM):
import config from './async-config.mjs';   // works even with top-level await inside

// 4. Replace __dirname / __filename:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Fix 4 – Pin or override the transitive dependency

If the top-level await is in a third-party package you cannot patch directly, pin to the last version before TLA was introduced, or force a patched version via your package manager's overrides mechanism.

// npm overrides — force lru-cache to a version without TLA
// package.json
{
  "overrides": {
    "lru-cache": "^11.3.1"
  }
}

// yarn resolutions
{
  "resolutions": {
    "lru-cache": "^11.3.1"
  }
}

// pnpm overrides
{
  "pnpm": {
    "overrides": {
      "lru-cache": "^11.3.1"
    }
  }
}

// Direct pin — install a specific safe version
npm install lru-cache@11.2.7

Fix 5 – Temporary: disable require(esm) feature

As a short-term workaround while you fix the root cause, you can disable the require(esm) feature entirely. This reverts Node.js to the pre-20.19 behavior where all require() of ESM throws ERR_REQUIRE_ESM.

# Command-line flag
node --no-experimental-require-module your-script.js

# Via environment variable (useful for tooling you cannot control directly)
NODE_OPTIONS="--no-experimental-require-module" npx firebase deploy
NODE_OPTIONS="--no-experimental-require-module" npx serverless deploy
This is not a real fix. It changes the error from ERR_REQUIRE_ASYNC_MODULE back to ERR_REQUIRE_ESM and prevents you from using the require(esm) feature entirely. Use only while implementing Fix 1, 2, or 3.

Fix 6 – TypeScript: change the module target

TypeScript with "module": "commonjs" emits require() calls at runtime. If the required package contains top-level await, this triggers ERR_REQUIRE_ASYNC_MODULE.

// tsconfig.json — switch to a non-CJS module format
{
  "compilerOptions": {
    "module": "nodenext",          // or "node16" / "es2022"
    "moduleResolution": "nodenext",
    "esModuleInterop": true,
    "target": "es2022"
  }
}

// Or use dynamic import() in TypeScript (works with any module target):
const { default: mod } = await import('async-esm-package');

Real-World Package Reference

Package Version that added TLA Fixed in Workaround
lru-cache 11.3.0 (diagnostics-channel-esm.mts) 11.3.1+ npm install lru-cache@11.2.7
firebase-tools Triggered by Node.js 22.12.0 upgrade (issue #8054) Later firebase-functions patch Pin Node.js to 22.11.0; use NODE_OPTIONS=--no-experimental-require-module
jsdom 29.0.1+ (transitive via @asamuzakjp/css-colorlru-cache@11.3.0) After lru-cache 11.3.1 update overrides: { "lru-cache": "11.2.7" }

Safe async-ESM Patterns

These patterns work whether you are calling from CJS or ESM.

Pattern A: Async factory function (replaces module-level TLA)

// database.mjs — safe to require() because no TLA at module level
let pool;

export async function getPool() {
  if (!pool) {
    const { createPool } = await import('./db-driver.mjs');
    pool = await createPool(process.env.DATABASE_URL);
  }
  return pool;
}

// caller — CJS or ESM
const { getPool } = require('./database.mjs');  // works: module is sync
const pool = await getPool();                    // async init deferred to call time

Pattern B: Promise-based module initializer (no TLA)

// metrics.mjs — initializes async without top-level await
let _channel;
const _ready = import('node:diagnostics_channel').then(dc => {
  _channel = dc.channel('app:requests');
});

export async function record(data) {
  await _ready;  // wait for init only on first call
  _channel.publish(data);
}

// This module has no TLA — _ready is a Promise stored in a variable,
// not awaited at the module's top scope. require() works fine.

Pattern C: Dynamic import() wrapper in CJS

// app.js (CommonJS)
// Wraps any async ESM module so CJS code can consume it

async function loadAsyncModule(specifier) {
  return import(specifier);
}

// Usage
const { default: config } = await loadAsyncModule('./async-config.mjs');

// Or with error handling:
let mod;
try {
  mod = await import('some-async-esm-package');
} catch (err) {
  if (err.code === 'ERR_REQUIRE_ASYNC_MODULE') {
    // Should not reach here — import() handles async ESM fine
    // This code catches the error if someone else require()'d it
  }
  throw err;
}

Debugging Checklist

  1. Run with node --experimental-print-required-tla your-script.js to get the exact file and line of the top-level await.
  2. Check the Node.js version: node --version. This error only appears on 20.19.0+ and 22.12.0+.
  3. If the TLA is in a third-party package, check the package's GitHub issues or changelog for a patch release that removes it.
  4. Run npm ls <package-name> to trace the full dependency chain from your code to the module with TLA.
  5. Verify that your error-handling code is not checking err.code === 'ERR_REQUIRE_ESM' — it needs to also handle 'ERR_REQUIRE_ASYNC_MODULE'.
  6. If you own the async module, check whether the top-level await is truly necessary or can be deferred to a factory function.
  7. For TypeScript: confirm tsconfig.json module is not "commonjs" if you are importing ESM packages with TLA.
  8. In Docker/CI: verify the Node.js version in the container image matches your local environment — the error only appears on 20.19+/22.12+.

ERR_REQUIRE_ASYNC_MODULE vs ERR_REQUIRE_ESM — Key Differences

ERR_REQUIRE_ESMERR_REQUIRE_ASYNC_MODULE
IntroducedNode.js 12 (stable)Node.js 20.19.0 / 22.12.0
TriggerAny require() of any ESM filerequire() of ESM with top-level await in graph
Synchronous ESMAlso throws this (pre-20.19)Does not apply — sync ESM loads fine
Async ESM (TLA)Also throws this (pre-20.19)Only error that fires in 20.19+
Diagnostic flagNone needed (the error message names the file)--experimental-print-required-tla shows the TLA location
Fiximport() or convert to ESM or pin to CJS versionimport() or remove TLA or convert to ESM

Frequently Asked Questions

What is ERR_REQUIRE_ASYNC_MODULE?

ERR_REQUIRE_ASYNC_MODULE is thrown when require() is called on an ES module that uses top-level await, or whose dependency graph contains a module with top-level await. Because require() is synchronous, it cannot evaluate a module that needs to wait for a Promise. The full error message is: Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead.

What is the difference between ERR_REQUIRE_ASYNC_MODULE and ERR_REQUIRE_ESM?

ERR_REQUIRE_ESM is the older error thrown when require() tries to load any ES module file. ERR_REQUIRE_ASYNC_MODULE is the newer, more specific error introduced in Node.js 20.19 and 22.12 when those versions enabled require(esm) by default — synchronous ESM can now be loaded with require(), but ESM with top-level await cannot, so it gets its own distinct error code.

How do I find which module has top-level await?

Use the --experimental-print-required-tla flag: node --experimental-print-required-tla your-script.js. Node.js will print the file path and exact line number of the top-level await expression that is blocking synchronous loading. This is essential when the offending module is buried deep in a transitive dependency chain.

Why did ERR_REQUIRE_ASYNC_MODULE appear after upgrading Node.js?

Before Node.js 20.19.0 and 22.12.0, all require() calls on ESM files threw ERR_REQUIRE_ESM regardless of top-level await. After those releases, require(esm) is enabled by default: synchronous ESM loads fine, but async ESM (containing top-level await) throws the new ERR_REQUIRE_ASYNC_MODULE. So upgrading Node.js can expose a previously hidden top-level await in a dependency.

Why does this happen with firebase-tools or lru-cache?

firebase-tools uses a synchronous require() in its runtime loader (src/runtime/loader.ts) that cannot handle async ESM when running on Node.js 22.12.0+. lru-cache v11.3.0 introduced top-level await in its ESM diagnostics build, breaking CJS packages that transitively loaded it (including jsdom). lru-cache fixed this in v11.3.1 by replacing TLA with a .then() initializer. Pin lru-cache to 11.2.7 if you cannot upgrade.

Can I use --no-experimental-require-module to fix this?

Yes, as a temporary workaround only. Adding --no-experimental-require-module (or NODE_OPTIONS=--no-experimental-require-module) reverts to pre-20.19 behavior where all require() of ESM throws ERR_REQUIRE_ESM. This does not fix the underlying problem — it just changes the error code and disables the entire require(esm) feature. Use only while implementing a real fix.

How do I fix ERR_REQUIRE_ASYNC_MODULE in a transitive dependency I cannot change?

Your options: (1) add an overrides field in your package.json to force a version of the offending sub-dependency that does not use top-level await; (2) pin the intermediate dependency that pulls in the async ESM module; (3) use dynamic import() at the point where the synchronous require() chain starts in your code; (4) convert your project root to ESM so everything loads asynchronously. Run --experimental-print-required-tla and npm ls <package> to identify the exact version and chain before deciding.

How do I fix ERR_REQUIRE_ASYNC_MODULE in Docker or CI?

Check the Node.js version in your container image. If it is 20.19+ or 22.12+, the require(esm) feature is active. Pinning your base image to node:20.18-alpine or node:22.11-slim will revert to the old behavior. The correct long-term fix is to replace require() with import() in your code. Set NODE_OPTIONS=--no-experimental-require-module as an environment variable in docker-compose.yml or your CI config as a quick workaround.

Related Errors