Because the ESM standard specifies that modules are loaded by URL and themselves might initialize async. Once anything is async the whole call stack into it is. See also various discussions of “colored functions” which have made the rounds again recently.
This is inaccurate, or at least misleading. Your code doesn’t begin executing until all of the modules are loaded so it doesn’t “make the call stack async” just because of the import system. Module loading that’s async from the point of view of user code is also supported, but it’s use is rare.
> Your code doesn’t begin executing until all of the modules are loaded.
This is inaccurate, or at least misleading.
This, or some aspect of it, is both possible and relatively common:
import foo from 'foo';
await foo.bar();
await Promise.all([
import('other'),
import('stuff'),
]);
// Admittedly this is less common but also valid!
import yet from 'more-stuff';
The asynchrony of ESM was controversial before it even became a standard. But it’s necessary because it allows network I/O. And most of the above patterns being relatively common is one of the major use cases for bundling, because it also introduces an indefinite waterfall.
In terms of rarity, dynamic import calls are already quite common for “lazy”/“suspense” or their equivalents in quite a lot of real world code, and likely to become moreso with React Server Components and other similar solutions deferring to server rendering.
Yes, an import statement is [semantically] blocking. But even so it’s important to know that it’s performing async I/O.
You only have to `await foo.bar()` if foo.bar was an async function already, the module system is irrelevant. You would still need to await it even if it was `require()`'d in.
Of course these have to be awaited, at this point you're explicitly trying to load new code while the program is running. It's no different than any other kind of async IO.
> import yet from 'more-stuff';
I assume this is included to imply that loading `yet` is blocked by the `await Promise.all` above it to show that import statements can run after module resolution. If this was your intent you are mistaken, that import statement is still resolved before execution begins.
// main.js
console.log('a')
await new Promise(res => setTimeout(res, 1000))
console.log('b')
import { foo } from "./foo.js";
// foo.js
export function foo() {
}
console.log('c')
Results in
c
a
[one second pause]
b
> Yes, an import statement is [semantically] blocking. But even so it’s important to know that it’s performing async I/O.
No, it really isn't. From the POV of the application code it's irrelevant. This is trivially proven by the existence of module bundlers which convert non-dynamic `import` statements into one big file that doesn't do anything asynchronous to initialize.
> You only have to `await foo.bar()` if foo.bar was an async function already, the module system is irrelevant. You would still need to await it even if it was `require()`'d in.
You cannot do that. Top-level await is forbidden because CJS require is explicitly synchronous.
> Works just fine, no `await` necessary.
I typed all of that on a phone so I erred on the side of brevity, but suppose either the Promise.all or the awaited imports depended on some result from importing foo.
> I assume this is included to imply that loading `yet` is blocked by the `await Promise.all` above it to show that import statements can run after module resolution. If this was your intent you are mistaken, that import statement is still resolved before execution begins.
I was trying to show that import statements are blocked by prior awaits. I’m not sure what you’re trying to show, but you’d need to log output after the import from foo.js to see how the asynchrony works for that import.
> No, it really isn't. From the POV of the application code it's irrelevant. This is trivially proven by the existence of module bundlers which convert non-dynamic `import` statements into one big file that doesn't do anything asynchronous to initialize.
Compare that to a non-bundled but otherwise similarly optimized app and look at the waterfall.
Node did make modules async (just ESM modules). They can’t/won’t make CJS async because their synchronous semantics are a guarantee they determined not to break well before ESM. If you want async modules, ESM is the solution for that.
Right. Working with Node.js I assume all dependencies are on the local disk. And that makes it simple and straightforward to use "require" in Node.js.
But sometimes I'd like the same code to work also in the browser. Then I should be using ES6, but it doesn't work very smoothly with Node.js and I fear there may be complications if I have to wait (or "await") for the modules to have loaded.
The ES6 "dynamic imports" add more considerations to the mix. Surely their exports can not be used until "await" is over?
I think it would simplify things if I didn't have to choose between module-systems when I really just want to choose between sync and async.