await
ECMAScript proposal: Top-level Status
This proposal is currently being considered to enter stage 0 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
The current implementation of async / await
only support the await
keyword inside of async
functions. As such we are beginning to see the following pattern emerge:
import ...
async function main() {
const dynamic = await import('./dynamic-thing.mjs');
}
main();
export ...
This pattern is reminiscent of the classic pattern of wrapping all of your code in a self executing function (fn(){}())
. This type of "magic" does not benefit new developers and creates excessive repeated code throughout the ecosystem.
Another risk with the above pattern is that it makes the body of the function asynchronus. If people are utilizing this pattern throughout their graph there will no longer be a deterministic execution order.
Another pattern that is begining to surface is exporting async function and awaiting the results of imports, which drasticly impacts our ability to do static analysis of the module graph.
export default async function (url) {
if (!fetch) {
const fetch = await import('./fetch-polyfill.mjs');
}
const data = await fetch(url);
return data;
}
Proposed solutions
await
blocks tree execution
Variant A: top-level In this proposed solution a call to top-level await
would block execution in the graph until it had resolved.
await
does not block sibling execution
Variant B: top-level 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.
Illustrative examples
Dynamic dependency pathing
const 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
const 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
let jQuery;
try {
jQuery = await import('https://cdn-a.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.com/jQuery');
}
FAQ
await
a footgun?
Isn't top-level 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 feature is in fact a foot gun
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.
Existing Ways to halt progress
Infinite Loops
for (const n of primes()) {
console.log(`${n} is prime}`);
}
Infinite series or lack of base condition means static control structures are vulnerable to infinite looping.
Infinite Recursion
const fibb = n => (n ? fibb(n - 1) : 1);
fibb(Infinity);
Proper tail calls allow for recursion to never overflow the stack. This makes it vulnerable to infinite recursion.
Atomics.wait
Atomics.wait(shared_array_buffer, 0, 0);
Atomics allow blocking forward progress by waiting on an index that never changes.
export function then
// a
export function then(f, r) {}
async function start() {
const a = await import('a');
console.log(a);
}
Exporting a then
function allows blocking import()
.
What about deadlock?
The main problem space in designing top level await is to aid in detecting and preventing forms of deadlock that can occur. 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.
Guarding against
Implementing a TDZ
// a
await import('b');
// implement a hoistable then()
export function then(f, r) {
r('not finished');
};
// remove the rejection
then = null;
// b
await 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.
// b
let a;
try {
a = await import('a');
} catch {
// do something
}
This mechanism could even be codified into import()
by making it reject if
the target module has not run to end of source text in Evaluation yet. At the current time a search for "export async function" on github produces over 5000 unique code examples of exporting an async function.
Specification
Implementations
- none yet