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).
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.Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await.node:internal/modules/cjs/loader: throw new ERR_REQUIRE_ASYNC_MODULE
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
| Property | Value | Notes |
|---|---|---|
err.code | 'ERR_REQUIRE_ASYNC_MODULE' | Use this to catch specifically in error handlers |
err.message | require() 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 version | require(sync ESM) | require(async ESM with TLA) |
|---|---|---|
| ≤ 20.18.x | Throws ERR_REQUIRE_ESM | Throws ERR_REQUIRE_ESM |
| 20.19.0+ | Works — returns module exports | Throws ERR_REQUIRE_ASYNC_MODULE |
| ≤ 22.11.x | Throws ERR_REQUIRE_ESM | Throws ERR_REQUIRE_ESM |
| 22.12.0+ | Works — returns module exports | Throws ERR_REQUIRE_ASYNC_MODULE |
| 23.x, 24.x, 26.x | Works — returns module exports | Throws ERR_REQUIRE_ASYNC_MODULE |
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;
}
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
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-color → lru-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
- Run with
node --experimental-print-required-tla your-script.jsto get the exact file and line of the top-levelawait. - Check the Node.js version:
node --version. This error only appears on 20.19.0+ and 22.12.0+. - If the TLA is in a third-party package, check the package's GitHub issues or changelog for a patch release that removes it.
- Run
npm ls <package-name>to trace the full dependency chain from your code to the module with TLA. - Verify that your error-handling code is not checking
err.code === 'ERR_REQUIRE_ESM'— it needs to also handle'ERR_REQUIRE_ASYNC_MODULE'. - If you own the async module, check whether the top-level
awaitis truly necessary or can be deferred to a factory function. - For TypeScript: confirm
tsconfig.jsonmoduleis not"commonjs"if you are importing ESM packages with TLA. - 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_ESM | ERR_REQUIRE_ASYNC_MODULE | |
|---|---|---|
| Introduced | Node.js 12 (stable) | Node.js 20.19.0 / 22.12.0 |
| Trigger | Any require() of any ESM file | require() of ESM with top-level await in graph |
| Synchronous ESM | Also 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 flag | None needed (the error message names the file) | --experimental-print-required-tla shows the TLA location |
| Fix | import() or convert to ESM or pin to CJS version | import() 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
ERR_REQUIRE_ESM— the predecessor error:require()of any ES module, thrown on Node.js < 20.19ERR_PACKAGE_PATH_NOT_EXPORTED— a subpath of the package is not in itsexportsmapMODULE_NOT_FOUND— the package cannot be located at allSyntaxError: Cannot use import statement outside a module— staticimportused in a file Node.js parses as CommonJSERR_UNSUPPORTED_DIR_IMPORT— ESM import targeting a directory instead of a specific file