- Asynchronicity of imported module is not visible
- Imports of async modules can have potentially performance impact and should be visible to the developer
- It's difficult to see which modules are affected by the top-level-await
- Once this is visible, developer can break the async chain with
import()
- Microticks between imports may be unexpected
- Developer don't know if modules are evaluated sync or async
- Execution order is no longer guaranteed when modules add top-level-awaits
- 2 module modes: normal/sync and async
import x, { a, b } from "m"
is only allowed for normal/sync modules (will throw an early Error otherwise)import await x, { a, b } from "m"
- imports sync and async modules
- execution of the remaining module body is delay until all modules imported with
import await
are resolved - A
import await
does NOT block otherimport await
s (likePromise.all
) - All
import await
s are hoisted (like with normalimport
). - Remaining module body is always scheduled in a new microtask when
import await
is used, even if all imported modules are sync. (import await
on sync modules behave likeawait Promise.resolve()
) import await
must not appear after a top-levelawait
, because this looks confusing
- Using top-level
await
orimport await
makes the module an async module - WebAssembly Modules are async modules
- Using
import await
makes it visible that- imported module may use async operations
- the scheduling of the remaining module body is changed
- importing this module may impact performance
- Disallowing
import
for async modules makes adding asynchronicity to a module intentionally a breaking change- It's possible to use asynchronicity internally when not visible on public API.
import()
will hide asynchronicity behind a Promise.
- It's possible to use asynchronicity internally when not visible on public API.
- Using
import await
makes it easy to see where using aimport()
may be a good fix for performance problems - Using
import await
makes it possible to transpile top-levelawait
to non-top-levelawait
code- The proposal can be implemented as babel-plugin to play with it
import await
allows support for Tree Shaking and Scope Hoisting, when supported by bundlers directly.
import b from "./b.js";
import await { c1, c2 } from "./c.js";
import await "./d.js";
import "./e.js";
console.log("f", b, c1, c2);
await connect();
console.log("g");
export const h = 42;
It's not exactly equal, but could be polyfilled this way.
import b from "./b.js";
import cPromise from "./c.js";
import dPromise from "./d.js";
import "./e.js";
export default Promise.all([cPromise, dPromise]).then(async ([cNs, dNs]) => {
console.log("f", b, cNs.c1, cNs.c2);
await connect();
console.log("g");
const h = 42;
return {
[Symbol.toStringTag]: "Module",
get h() {
return h;
}
};
});
// a.js
import "./b.js";
import await "./c.js";
import await "./e.js";
import "./g.js";
console.log(30);
// b.js
console.log(1);
// c.js
import "./d.js";
console.log(3);
// d.js
console.log(2);
// e.js
import await "./f.js";
console.log(10);
await 1;
console.log(20);
// f.js
console.log(4);
// g.js
console.log(5)
Will print 1, 2, 3, 4, 5, microtick, 10, microtick, 20, microtick, 30.
Because all normal imports (and normal imports behind import await
) are evaluated early it's possible to create a new concatenated module from multiple imported modules.
The example above is equal to:
console.log(1);
console.log(2);
console.log(3);
console.log(4);
console.log(5);
const fPromise = Promise.resolve();
const ePromise = Promise.all([fPromise]).then(async () => {
console.log(10);
await 1;
console.log(20);
});
const cPromise = Promise.resolve();
Promise.all([cPromise, ePromise]).then(() => {
console.log(30);
});
// db-connection.js
await connectToDB(URL);
export const dbCall = async data => { ... }
// ...
// UserApi.js
import await { dbCall } from "./db-connection.js";
export const createUser = async name => {
// ...
await dbCall({ ... });
}
// Actions.js
const UserApi = import("./UserApi.js");
export const CreateUserAction = async name => {
const { createUser } = await UserApi;
await createUser(name);
};
// UserComponent.js
import { CreateUserAction } from "./Actions.js";
CreateUserAction("John");
import await storage from "./kv-storage.js"
let storage;
try {
storage = (await import("std:kv-storage")).default;
} catch {
storage = (await import("./kv-storage-polyfill.js")).default;
}
export { storage as default };
Requiring an async module will return a Promise
to CommonJS code
const kvStoragePromise = require("./kv-storage.js");
const somewhere = async () => {
const { default: storage } = await kvStoragePromise;
};
Top-level await
is not allowed in CommonJS code.
await import { named1, named2 } from "./module.js"; // Parsing problematic
import await { named1, named2 } from "./module.js";
import { named1, named2 } await from "./module.js";
import { named1, named2 } from await "./module.js"; // Weird awaiting of string
import await def, { named1, named2 } from "./module.js";
This looks like a really good way of doing it.
Just a note on transpilation (not the spec itself):
The Promise should probably not be default exported but exported using some key
__someTopLevelAwaitPromiseKey
so that you can't just do a normal import on the transpiled module or evenimport {then} from './topLevelAwaitModule'
.