top-level await for dynamic import of ES6 Module is pending forever when module contains import

906 Views Asked by At

I am trying to dynamically import an ES6 Module (Chrome 109 on Windows 10, retested with Firefox 108). And I want to use a top-level await to wait for the module to be available.

This is a showcase main.js, it is loaded by <script type="module" src="main.js"></script>. I split import and await so that I could dump the promise with console.log.

let modulePromise = import("./test_dummy.js");
setTimeout(() => console.log("timeout", modulePromise), 5000);

let module = await modulePromise;
module.test();

test_dummy.js looks like this:

// import { Foo } from "./foo.js";
console.log("Here is your test dummy");
export function test() {
   console.log("Hello StackOverflow");
}

As long as the Foo-import is in a comment, this is working fine. But as soon as I uncomment it, the module is loaded (I see this in the DevTools Network tab), but not executed (the first console.log is not shown) and the promise stays pending (my setTimeout handler says so). And no, it is not simply waiting for a slow network, I am loading this from localhost.

The problem is the top-level await, because when I replace it with a .then() callback, the module loads. I tried some elaborate workarounds, to encapsulate the import in an async function. I even hid it in another Promise. No luck.

The attempt to top-level await a Promise that depends directly or indirectly on a subsequent import seems to be toxic. Why is this? At first I believed I had found a Chrome bug, but Firefox exhibits the same behaviour.

Greetings, Rolf

Edit: I'll try to clarify. test_dummy.js and foo.js are loaded, I see this in the network tab of DevTools. But they are not executed (the first console.log does not show). The content of foo.js does not really matter (except that it should be error-free, of course). For testing, I used a dummy module with just a console.log in it.

As shown in source code, the test_dummy.js module exports a function named test, not a promise. The promise that I am awaiting is the module promise, created my import(). When I await it, I get a Module object, which contains the exported data.

I tried something different:

run_module("./test_dummy.js");

async function run_module(moduleName) {
   (await import(moduleName)).test();
}

This works as intended. But: run_module() should return a promise, and I should be able to await it on top-level:

await run_module("./test_dummy.js");
...

But when I do that, everythings stops working again. As I said: The attempt to top-level await a Promise that depends directly or indirectly on an import that is triggered inside a dynamic import seems to be toxic.

Using all of my code inside run_module is my workaround for now. But I want to know why the top-level await destroys everything.

1

There are 1 best solutions below

1
On

After Bergis comment, I think I should post this as an (partial) answer.

Yes, I have circular references.
Yes, I'll wear sackcloth. Can somebody dump some ashes on my head, please?

main.js dynamically imports ./test_dummy.js, and test_dummy.js statically imports ./foo.js, and I didn't mention that foo.js statically imports a helper object from ./main.js. I thought this to be irrelevant, because main.js is already loaded and should be imported from memory. There is no network request visible for it.

As I started the project, all imports were static, and the circular reference wasn't a problem.

When I remove the backreference and inject my testhelper object as an argument into the test() function call, the top-level await works as intended.

But it's still a slightly sore point. Why is the back-import killing the top-level await? Why is the async function workaround working at all?