Skip to content

Instantly share code, notes, and snippets.

@Rich-Harris
Created December 20, 2017 15:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Rich-Harris/1c44b7caed5c4ac2aadb2e2cd1028fdc to your computer and use it in GitHub Desktop.
Save Rich-Harris/1c44b7caed5c4ac2aadb2e2cd1028fdc to your computer and use it in GitHub Desktop.
import from async

The fundamental problem with top-level await is that it prevents two unrelated modules doing asynchronous work in parallel, because module evaluation order is strictly deterministic.

What if we provided an escape valve that relaxed that constraint?

// main.js
import foo from async './foo.js';
import bar from async './bar.js';
import baz from './baz.js';

console.log(foo, bar, baz);
// foo.js
export default await fetch('/foo.json').then(r => r.json());
console.log('got foo');
// bar.js
export default await fetch('/bar.json').then(r => r.json());
console.log('got bar');
export default 'BAZ';
console.log('got baz');

By using from async, we're allowing foo.js and bar.js to use top-level await. (If a consumer were to just use from, it would be an error.)

The output of this (assuming foo.json contains "FOO" and bar.json contains "BAR") could be either of these:

BAZ
FOO
BAR
FOO BAR BAZ
BAZ
BAR
FOO
FOO BAR BAZ

This is roughly analogous to wrapping foo.js and bar.js in async functions, except that main.js awaits both of them:

async function foo() {
  const result = await fetch('/foo.json').then(r => r.json());
  console.log('got foo');
  return result;
}

async function bar() {
  const result = await fetch('/bar.json').then(r => r.json());
  console.log('got bar');
  return result;
}

function baz() {
  const result = 'BAZ';
  console.log('got baz');
  return result;
}

function main() {
  Promise.all([foo(), bar(), baz()]).then(([foo, bar, baz]) => {
    console.log(foo, bar, baz);
  });
}

I've argued elsewhere that determinism is important, but maybe if we can opt-in to non-determinism with our eyes open, it's okay?

@MylesBorins
Copy link

Would entry points be able to use top level await?

What type of errors are you imagining would happen if you tried to import an async module without the async syntax?

Are there any concerns around inconsistent execution between multiple runs?

@Rich-Harris
Copy link
Author

Would entry points be able to use top level await?

I don't see why not. The concerns I and others have expressed are all about nested (unrelated) modules blocking each other, which doesn't apply to entry modules. Hell, I wrote a thing (lit-node) that allows you to use TLA in entry modules, because it's incredibly useful in a scripting context with none of the drawbacks.

What type of errors are you imagining would happen if you tried to import an async module without the async syntax?

If I use await in a non-async function I get this:

screen shot 2017-12-20 at 1 50 58 pm

I'm envisaging something along those lines — 'await is only valid syntax if imported using import ... from async 'specifier' or dynamic import()'. Wordy but you get the idea. I'm sure there are lots of corner cases to consider here.

Are there any concerns around inconsistent execution between multiple runs?

Definitely. I've argued that it's a huge problem. But if you're aware of possible race conditions — and the explicit from async is meant to enforce that awareness — you can deal with them. Basically I think it boils down to 'make damn sure you're explicit about your dependencies', i.e. don't depend on a polyfill that may or may not exist yet, and avoiding circular deps

@ExE-Boss
Copy link

ExE-Boss commented Oct 22, 2019

This feels like a good idea to me, so I’ve opened tc39/proposal‑top‑level‑await#134.

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