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
User | Most Recent Commit | # Commits |
---|
Other Committers
User | Most Recent Commit | # Commits |
---|
await
This proposal is currently in stage 2 of the TC39 process.
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.
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...
;
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.
await
blocks tree executionIn 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.
await
does not block sibling executionIn 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.
await
can only be used in modules without exportsMany 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.
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.
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.
jQuery;
{
uery = await import('https://cdn-a.com/jQuery');
tch {
uery = await import('https://cdn-b.com/jQuery');
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.
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.
(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.
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.
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()
.
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.
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.
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.
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>