Repository navigation
Top-level await + dynamic import + cyclic import causes "unsettled TLA" error #55468
Description
Activity
foo.jshas already been linkedSure but linking is only important for static imports, no? Because static imports need to know in advance the exported names of each dependencies.
import {} from "./foo.js"doesn't import anything […], so it should not block the evaluation ofbar.js.The
importstatement itself is a no-op during evaluation, so it's not blocking anything. However,bar.jssource text is never evaluated because it's waiting for its dependencies (i.e../foo.js) to be evaluated first, which is blocked on the TLA.
My understanding is that the bug in on Bun, and Node.js behavior matches how I interpret the spec.@aduh95 Thanks for your explanation.
However, bar.js source text is never evaluated because it's waiting for its dependencies to be evaluated first ...
But I'm just a bit confused that, in "static" cyclic dependencies, for example,
// a.js import { b } from "./b.js"; await new Promise((r) => setTimeout(r, 1000)); console.log("module a loaded"); // b.js import * as a from "./a.js"; console.log(a); export const b = 1;
The
console.log(a)is executed beforeconsole.log("module a loaded"). Seems thatb.jsdoes not wait the dependencyimport ... from "./a.js"to finished. So why things got changed in a dynamic import context?Take the following shell script:
mkdir repro cd repro cat -> a.mjs <<'EOF' import { b } from "./b.mjs"; await new Promise((r) => setTimeout(r, 1000)); console.log("module a loaded"); EOF cat -> b.mjs <<'EOF' import * as a from "./a.mjs"; console.log(a); export const b = 1; EOF echo "a.mjs is the entry point:" node a.mjs echo echo "b.mjs is the entry point:" node b.mjs cd .. rm -rf repro
This outputs:
a.mjs is the entry point: [Module: null prototype] { } module a loaded b.mjs is the entry point: module a loaded [Module: null prototype] { }As you can see, the order depends on which is the entry point, and the entry point is always executed last.
So why things got changed in a dynamic import context?
Because there's no cyclic module graph when you use dynamic imports. Cyclic module has its own section of the spec, it's not surprising you get different results with static import and dynamic imports.
Reacted by GuyutongxueSo why things got changed in a dynamic import context?
This was considered but ultimately rejected as it was favoured that dynamic import should always return a fully evaluated module.
Spec-wise, it wouldn't actually be that complicated to have an option to
import(...)to allow partially initialized namespaces (just like static import already allows), but it would need be a TC39 proposal to actually happen.Technically I think it would actually be conforming for hosts to have this behaviour by an import attribute (e.g.
import("./foo.js", { with: { detectCyclicImportAndReturnPartialNamespace: "true" } })).I don't know if there's really enough demand that Node (let alone other hosts) would add this (though I have seen basically this exact issue be raised a couple times before, so maybe the demand is high enough, but someone would have to implement it).
I've encountered this issue because Vite is building TLA +
import()to a cyclic module graph.Original source:
// main.ts await import("./module_1"); // module_1.ts import("./moudle_2");
builds to:
// main.js export const __vite_preload = /* ... */; await __vite_preload(() => import("./module_1.js")); // module_1.js import { __vite_preload } from "./main.js"; __vite_preload(() => import("./module_2.js"));
The corresponding Vite issue is vitejs/vite#18156 -- this issue does affect a number of user.
Just ran into this issue. It does work on
arm64but notamd64.Found out the hard way after deploying to AWS ECS which uses
amdby default.
While our local dev machines are on Applearm.Tested on Node 22.14.0 using docker with
--platform linux/amd64.I’ve reproduced this issue in Node.js v23.11.0 with a minimal test case involving top-level await, dynamic import(), and a circular dependency. Unlike #55468, where Deno also fails, Deno succeeds here, highlighting a Node.js-specific ESM loader bug.
Platform:
Debian GNU/Linux 12 (bookworm), 6.6.67-06628-g571b599e617d, aarch64 (Chromebook)
Directory Structure:
folder/ ├── package.json ├── main.js ├── mod.jspackage.json:
{"type": "module"}main.js:
export function fn() {} await Promise.resolve(); async function loadModule() { const mod = await import("./mod.js"); console.log(mod.default); } loadModule();
mod.js:
import { fn } from "./main.js"; export default "It works!";
Node.js v23.11.0 Output:
Warning: Detected unsettled top-level await await Promise.resolve(); ^Deno Output:
It works!Workaround:
export function fn() {} Promise.resolve().then(async () => { async function loadModule() { const mod = await import("./mod.js"); console.log(mod.default); } loadModule(); });
Additional Notes:
- The bug persists from v22.10.0 (Top-level await + dynamic import + cyclic import causes "unsettled TLA" error #55468) to v23.11.0.
- Deno’s success suggests Node.js’s ESM loader struggles with circular dependencies and top-level await.
- Wrapping dynamic import() in a function (loadModule) seems to help Deno resolve the cycle, but Node.js still fails due to top-level await.
- This minimal test case complements the original repro and isolates the Node.js issue. Happy to test fixes or provide further details!
- added a commit that references this issue
on Oct 8, 2025 - added a commit that references this issue
on Oct 8, 2025 I resolved this issue by enabling
splitting: true.github-actions commented
on Jul 20, 2026 on Jul 20, 2026 – with GitHub ActionsContributorMore actionsThis issue has been marked as stale due to 90 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.- addedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Jul 20, 2026 github-actions commented
on Aug 20, 2026 on Aug 20, 2026 – with GitHub ActionsContributorMore actionsThis issue has been automatically closed after 30 days of inactivity following its stale status (no activity for a total of 120 days).
If this is still relevant, feel free to reopen it or leave a comment with additional details so we can continue the discussion.
Version
v22.10.0
Platform
Subsystem
No response
What steps will reproduce the bug?
Create following files:
package.json:{ "private": true, "type": "module" }foo.js:bar.js:Then run
node foo.js.How often does it reproduce? Is there a required condition?
Every time.
What is the expected behavior? Why is that the expected behavior?
Outputs
It works!.import {} from "./foo.js"doesn't import anything, andfoo.jshas already been linked, so it should not block the evaluation ofbar.js.What do you see instead?
And the process exited with error code
13.Additional information
Behavior of other runtime:
It works!successfully.It works!successfully.I'm not sure what is the correct behavior that ECMAScript specification defines.