tc39/proposal-top-level-await

Name: proposal-top-level-await

Owner: Ecma TC39

Description: top-level `await` proposal for ECMAScript (stage 2)

Created: 2018-01-22 19:32:45.0

Updated: 2018-05-24 08:52:04.0

Pushed: 2018-05-23 20:47:02.0

Homepage: https://tc39.github.io/proposal-top-level-await/

Size: 69

Language: HTML

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

ECMAScript proposal: Top-level await

Status

This proposal is currently in stage 2 of the TC39 process.

Background

The async / await proposal was originally brought to committee in January of 2014. In April of 2014 it was discussed that the keyword await should be reserved in the module goal for the purpose of top-level await. In July of 2015 the async / await proposal advanced to Stage 2. During this meeting it was decided to punt on top-level await to not block the current proposal as top-level await would need to be “designed in concert with the loader”.

Since the decision to delay standardizing top-level await it has come up in a handful of committee discussions, primarily to ensure that it would remain possible in the language.

Motivation
top-level main and IIAFEs

The current implementation of async / await only support the await keyword inside of async functions. As such many programs that are utilizing await now make use of top-level main function:

rt ...
c function main() {
nst dynamic = await import('./dynamic-thing.mjs');
nst data = await fetch(dynamic.url);
nsole.log(data);

();
rt ...

This pattern is creating an abundance of boilerplate code in the ecosystem. If the pattern is repeated throughout the module graph it degrades the determinism of the execution.

This pattern can also be immediately invoked.

rt static1 from './static1.mjs';
rt { readFile } from 'fs';

nc () => {
nst dynamic = await import('./dynamic' + process.env.something + '.js');
nst file = JSON.parse(await readFile('./config.json'));

 main program goes here...
;
completely dynamic modules

Another pattern that is beginning to surface is exporting async function and awaiting the results of imports, which drastically impacts our ability to do static analysis of the module graph.

rt default async () => {
 import other modules like this one
nst import1 = await (await import('./import1.mjs')).default();
nst import2 = await (await import('./import2.mjs')).default();

 compute some exports...
turn {
export1: ...,
export2: ...


At the current time a search for “export async function” on github produces over 5000 unique code examples of exporting an async function.

Proposed solutions
Variant A: top-level await blocks tree execution

In this proposed solution a call to top-level await would block execution in the graph until it had resolved.

In this implementation you could consider the following

rt a from './a.mjs'
rt b from './b.mjs'
rt c from './c.mjs'

ole.log(a, b, c);

If each of the modules above had a top level await present the loading would have similar execution order to

nc () => {
nst a = await import('./a.mjs');
nst b = await import('./b.mjs');
nst c = await import('./c.mjs');

nsole.log(a, b, c);
;

Module a would need to finish executing before b or c could execute.

Variant B: top-level await does not block sibling execution

In this proposed solution a call to top-level await would block execution of child nodes in the graph but would allow siblings to continue to execute.

In this implementation you could consider the following

rt a from './a.mjs'
rt b from './b.mjs'
rt c from './c.mjs'

ole.log(a, b, c);

If each of the modules above had a top level await present the loading would have similar execution order to

nc () => {
nst [a, b, c] = await Promise.all([
import('./a.mjs'),
import('./b.mjs'),
import('./c.mjs')
;
nsole.log(a, b, c);
;

Modules a, b, and c would all execute in order up until the first await in each of them; we then wait on all of them to resume and finish evaluating before continuing.

Optional Constraint: top-level await can only be used in modules without exports

Many of the use cases for top-level await are for bootstrapping an application. Enforcing that top-level await could only be used inside of a module without exports would allow individuals to use many of the patterns they would like to use in application bootstrapping while avoiding the edge cases of graph blocking or deadlocks.

With this constraint the implementation would still need to decide between Variant A or B for sibling execution behavior.

Illustrative examples
Dynamic dependency pathing
t strings = await import(`/i18n/${navigator.language}`);

This allows for Modules to use runtime values in order to determine dependencies. This is useful for things like development/production splits, internationalization, environment splits, etc.

Resource initialization
t connection = await dbConnector();

This allows Modules to represent resources and also to produce errors in cases where the Module will never be able to be used.

Dependency fallbacks
jQuery;
{
uery = await import('https://cdn-a.com/jQuery');
tch {
uery = await import('https://cdn-b.com/jQuery');

FAQ
Isn't top-level await a footgun?

If you have seen the gist you likely have heard this critique before. My hope is that as a committee we can weigh the pros / cons of the various approaches and determine if the benefits of the feature outweigh the risks.

Halting Progress

Variant A would halt progress in the module graph until resolved.

Variant B offers a unique approach to blocking, as it will not block siblings execution.

The Optional Constraint would alleviate concerns of halting process.

Existing Ways to halt progress Infinite Loops
(const n of primes()) {
nsole.log(`${n} is prime}`);

Infinite series or lack of base condition means static control structures are vulnerable to infinite looping.

Infinite Recursion
t fibb = n => (n ? fibb(n - 1) : 1);
(Infinity);

Proper tail calls allow for recursion to never overflow the stack. This makes it vulnerable to infinite recursion.

Atomics.wait
ics.wait(shared_array_buffer, 0, 0);

Atomics allow blocking forward progress by waiting on an index that never changes.

export function then

rt function then(f, r) {}
js
c function start() {
nst a = await import('a');
nsole.log(a);

Exporting a then function allows blocking import().

What about deadlock?

A potential problem space to solve for in designing top level await is to aid in detecting and preventing forms of deadlock that can occur. For example awaiting on a cyclical dynamic import could introduce deadlock into the module graph execution.

Deferring to current behavior

There is already the potential to introduce deadlock into the module graph execution via dynamic import. Rather than trying to solve for specific types of deadlock we can simply defer to the current behavior and accept deadlock as a possibility.

Implementing a TDZ

All examples below will use a cyclic import() to a graph of 'a' -> 'b', 'b' -> 'a' with both using top level await to halt progress until the other finishes loading. For brevity the examples will only show one side of the graph, the other side is a mirror.


t import('b');

mplement a hoistable then()
rt function then(f, r) {
'not finished');


emove the rejection
 = null;
js

t import('a');

Having a then in the TDZ is a way to prevent cycles while a module is still evaluating. try{}catch{} can also be used as a recovery or notification mechanism.


a;
{
= await import('a');
tch {
 do something

Given the amount of boilerplate code required to implement this behavior, one could argue that the language specification should codify this behavior by saying that import() should always reject if the imported module has not finished evaluating. However, that behavior would make import() a less predictable abstraction for importing asynchronous modules, since it would deprive modules using top-level await safely (that is, without creating dependency cycles) of the chance to finish async work, just because they were imported using import(). Given the likelihood that these “safe” modules will vastly outnumber modules using top-level await unsafely, rejection should not be a blanket policy, but instead an option for handling specific cases of circular imports.

Instead of import() rejecting if the imported module is suspended on an await expression, it might be useful if there was a way for dynamic import() to obtain a reference to the incomplete module namespace object, similar to how CommonJS require returns an incomplete module.exports object in the event of cycles. As long as the incomplete namespace object provides enough information, or the namespace object isn't used until later, this strategy would allow the application to keep making progress, rather than deadlocking. In contrast to CommonJS, ECMAScript modules can better enforce TDZ restrictions (preventing export use before initialization), and can throw more useful static errors. However, preventing any access to the incomplete namespace object would be a loss of functionality compared to CommonJS.

Doesn't Variant B break the Polyfill use case?

There currently isn't a way to dynamically import polyfills and ensure they are loaded before other code executes.

With Variant A individuals would be able to dynamically import polyfills and be guaranteed they would load before other code executes.

With Variant B there is no guarantee that other code wouldn't execute prior to the polyfill being available. Module goal scripts are deferred by default with execution order between multiple scripts being guaranteed. In the case of wanting to have dynamic polyfills that are guaranteed to execute prior to application code one could import polyfills as a seperate scipt tag in their html

 This script will execute before? -->
ipt type="module" src="polyfills.mjs"></script>
 ?this script. -->
ipt type="module" src="index.mjs"></script>
Specification
Implementations
References

This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.