node
node copied to clipboard
Unexpected early exit (code 13) when trying to `import {}` from a dynamic `import()`
Version
v18.9.0
Platform
Microsoft Windows NT 10.0.19044.0 x64
Subsystem
Also tested in a Linux devcontainer (Linux docker-desktop 5.10.16.3-microsoft-standard-WSL2)
What steps will reproduce the bug?
// package.json
{
"type": "module",
"scripts": {
"start": "node server.js"
}
}
// server.js
import * as path from "path";
import * as url from "url";
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const BASE_URL = new URL("http://localhost:8080");
const exports = await import(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";
export async function get(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.log
d.
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
.
Additional information
Originating issue: https://github.com/TypeStrong/ts-node/discussions/1883
// server.js import * as path from "path"; import * as url from "url"; const __filename = url.fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export const BASE_URL = new URL("http://localhost:8080"); const exports = await import(url.pathToFileURL(path.join(__dirname, "index.js")).toString()); console.log(exports); // <-- we never get here
FWIW this can be simplified to:
// server.js
export const BASE_URL = new URL("http://localhost:8080");
const exports = await import(new URL("./index.js", import.meta.url));
console.log(exports); // <-- we never get here
What is the expected behavior?
The
BASE_URL
getsconsole.log
d.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.
I see your original issue.
I believe the code won't works because you are trying to transit from CJS
to ESM
in TypeScript
?
The same code will works in CJS
but not ESM
.
Because
index.js
depends onserver.js
andserver.js
execution is "blocked" by the top-level await, you end up in a soft lock and theimport()
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:
// index.js
import { BASE_URL } from "./server.js";
// ...etc
// server.js
import * as path from "path";
import * as url from "url";
import * as exports from "./index.js";
// ...etc
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:
// a.js
import * as modB from "./b.js";
console.log("Executing a.js");
console.log(modB);
export const a = "a";
// b.js
import { a } from "./a.js";
console.log("Executing b.js");
console.log(modA);
export const b = "b";
And so if you run it, this happens:
> node a.js
Executing b.js
[Module: null prototype] { a: <uninitialized> }
Executing a.js
[Module: null prototype] { b: 'b' }
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.
const modA = await import("./a.js");
console.log(modA);
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.