You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Unexpected early exit (code 13) when trying to import {} from a dynamic import() #44601
// server.jsimport*aspathfrom"path";import*asurlfrom"url";const__filename=url.fileURLToPath(import.meta.url);const__dirname=path.dirname(__filename);exportconstBASE_URL=newURL("http://localhost:8080");constexports=awaitimport(url.pathToFileURL(path.join(__dirname,"index.js")).toString());console.log(exports);// <-- we never get here
// index.js// The error appears to be contingent on this import:import{BASE_URL}from"./server.js";exportasyncfunctionget(request,response){console.log(BASE_URL);}
How often does it reproduce? Is there a required condition?
Always.
What is the expected behavior?
The BASE_URL gets console.logd.
What do you see instead?
Unexpected early exit (code 13) on the dynamic import().
No error. Also no error if wrapped in a try...catch.
// server.jsimport*aspathfrom"path";import*asurlfrom"url";const__filename=url.fileURLToPath(import.meta.url);const__dirname=path.dirname(__filename);exportconstBASE_URL=newURL("http://localhost:8080");constexports=awaitimport(url.pathToFileURL(path.join(__dirname,"index.js")).toString());console.log(exports);// <-- we never get here
FWIW this can be simplified to:
// server.jsexportconstBASE_URL=newURL("http://localhost:8080");constexports=awaitimport(newURL("./index.js",import.meta.url));console.log(exports);// <-- we never get here
What is the expected behavior?
The BASE_URL gets console.logd.
What do you see instead?
Unexpected early exit (code 13) on the dynamic import().
To me that seems like the expected behavior, code 13 means unfinished top-level await (see https://nodejs.org/api/process.html#exit-codes). Because index.js depends on server.js and server.js execution is "blocked" by the top-level await, you end up in a soft lock and the import() promise never settles. See https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs for more information on this, but AFAIU Node.js follows the ECMAScript spec here.
Because index.js depends on server.js and server.js execution is "blocked" by the top-level await, you end up in a soft lock and the import() promise never settles.
Yes, if you have a cycle you shouldn't use dynamic import at the top level to load the other module. In general you can just use a top-level import and things won't deadlock:
The reason static import works (and CJS equivalent for that matter) is that static import is allowed to return a namespace that isn't fully evaluated. i.e. You can get exports BEFORE the module you're importing from has evaluated:
Notice that modA has a being <uninitialized>, that's because a.js hasn't run yet so const a = "a"; hasn't set a to a value yet.
Dynamic import is different from static import in that it only ever returns fully initialized modules, so it can't return early with a partially initialized module. i.e. This isn't allowed to print [Module ...] { a: <uninitialized> } because import() always returns fully initialized modules.
In principle dynamic import()could have returned partially initialized modules, however the TC39 decided against it as it would mean import() would behave differently in some circumstances, and static import already exists to allow partially initialized circular modules anyway.
As explained already, top-level await on a dynamic import that involves circular dependency would lead to deadlock in the JS execution, and in Node.js since the process is only kept alive by I/O (or, active handles in the event loop), not by pending tasks, a deadlock purely on the JS side would just leave the event loop clear of any active handles, therefore Node.js would just exit without executing any code following the problematic top-level await (in browsers, where there is not a concept of "exit on completion", the deadlock would just prevent any code below the problematic top-level await from ever executing, until it's signaled to exit when you e.g. close the tab, and still won't execute those code). This is exactly why the exit code 13 was invented to warn about this case. So it's not a bug but a known limitation.
Activity
aduh95 commentedon Sep 12, 2022
FWIW this can be simplified to:
To me that seems like the expected behavior, code 13 means unfinished top-level await (see https://nodejs.org/api/process.html#exit-codes). Because
index.jsdepends onserver.jsandserver.jsexecution is "blocked" by the top-level await, you end up in a soft lock and theimport()promise never settles. See https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs for more information on this, but AFAIU Node.js follows the ECMAScript spec here.climba03003 commentedon Sep 14, 2022
I see your original issue.
I believe the code won't works because you are trying to transit from
CJStoESMinTypeScript?The same code will works in
CJSbut notESM.Jamesernator commentedon Sep 20, 2022
Yes, if you have a cycle you shouldn't use dynamic import at the top level to load the other module. In general you can just use a top-level import and things won't deadlock:
The reason static import works (and CJS equivalent for that matter) is that static import is allowed to return a namespace that isn't fully evaluated. i.e. You can get
exportsBEFORE the module you're importing from has evaluated:And so if you run it, this happens:
Notice that
modAhasabeing<uninitialized>, that's becausea.jshasn't run yet soconst a = "a";hasn't setato a value yet.Dynamic import is different from static import in that it only ever returns fully initialized modules, so it can't return early with a partially initialized module. i.e. This isn't allowed to print
[Module ...] { a: <uninitialized> }becauseimport()always returns fully initialized modules.In principle dynamic
import()could have returned partially initialized modules, however the TC39 decided against it as it would meanimport()would behave differently in some circumstances, and static import already exists to allow partially initialized circular modules anyway.dario-piotrowicz commentedon Feb 2, 2025
Based on @aduh95 and @Jamesernator comments I agree that this looks like the correct behavior
Based on that, is this issue still relevant? or should it be closed? (I don't think there's any actionable item here?)
joyeecheung commentedon Feb 4, 2025
As explained already, top-level await on a dynamic import that involves circular dependency would lead to deadlock in the JS execution, and in Node.js since the process is only kept alive by I/O (or, active handles in the event loop), not by pending tasks, a deadlock purely on the JS side would just leave the event loop clear of any active handles, therefore Node.js would just exit without executing any code following the problematic top-level await (in browsers, where there is not a concept of "exit on completion", the deadlock would just prevent any code below the problematic top-level await from ever executing, until it's signaled to exit when you e.g. close the tab, and still won't execute those code). This is exactly why the exit code 13 was invented to warn about this case. So it's not a bug but a known limitation.