Skip to content

Instantly share code, notes, and snippets.

@sokra
Created March 12, 2019 11:37
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sokra/33f3db1b2714e9a6720f890842c47ae6 to your computer and use it in GitHub Desktop.
Save sokra/33f3db1b2714e9a6720f890842c47ae6 to your computer and use it in GitHub Desktop.

Top-level await

Concerns to the original proposal

  • 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

Proposed changes

  • 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 other import awaits (like Promise.all)
    • All import awaits are hoisted (like with normal import).
    • 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 like await Promise.resolve())
    • import await must not appear after a top-level await, because this looks confusing
  • Using top-level await or import await makes the module an async module
  • WebAssembly Modules are async modules

Reasoning

  • 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.
  • Using import await makes it easy to see where using a import() may be a good fix for performance problems
  • Using import await makes it possible to transpile top-level await to non-top-level await 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.

Example

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;

Semantic Alternative

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;
		}
	};
});

Execution order

// 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.

Scope Hoisting

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);
});

Example

// 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");

Example

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 };

Interop with CommonJS

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.

Alternative Syntax

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

Default export

import await def, { named1, named2 } from "./module.js";
@jeysal
Copy link

jeysal commented Mar 12, 2019

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 even import {then} from './topLevelAwaitModule'.

@anilanar
Copy link

Just to clarify how early evaluation of async modules work, what would be logged for the following?

// a.js
import await './b.js';
import await './c.js';

// b.js
console.log('b');
await 1;

// c.js
import './d.js';
await 1;

// d.js
console.log('d');

And what about cyclic async imports?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment