Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] import await #60

Closed
wants to merge 2 commits into from

Conversation

@littledan
Copy link
Member

commented Mar 15, 2019

This patch updates the explainer to be in terms of requiring modules
containing top-level await to be imported with import await. Spec
text not yet written.

This patch updates the explainer to be in terms of requiring modules
containing top-level await to be imported with `import await`. Spec
text not yet written.
@littledan

This comment has been minimized.

Copy link
Member Author

commented Mar 15, 2019

Thanks to @sokra and everyone at the JSKongress 2019 Deep Track for developing this idea!

`async` functions and top-level `await` can be thought of as being "viral" because they have to be considered all the way up the call stack/dependency chain. You can't just change an ordinary function to an async function without considering its users, who would probably be broken, unless they happen to always `await` the result. It would be quite unusual for them to do such defensive `await`-ing, and we don't seem to see that catching on as an idiom.

The same applies to top-level `await`--their dependencies need to `await` the module when importing, and recursively so. This means that, just like changing a function into an `async` function, it's a semver-major change to add top-level `await` to a module. We will need to document this effect carefully, to avoid potential ecosystem breakage.

## History

This comment has been minimized.

Copy link
@sokra

sokra Mar 15, 2019

What's the difference between import await "module" and await import("module")?

import await starts fetching during the Fetch and Parse phase, while await import() starts fetching during the Evaluation phase.

Multiple import await will run in parallel, while multiple await import() will run sequential.

Exports of import await can be analysed statically and incorrect export names will result in a SyntaxError. Analyzing await import() is harder and incorrect export names will result in undefined and a RuntimeError.

import await is hoisted, await import() is an Expression and not hoisted.

import await can only be placed on top-level, while await import() can be placed at top-level or in async functions at any location where an Expression is allowed.

import await must be used with a String Literal, while await import() can be used with any expression as argument (await import(`./data/${value}.js`))

This comment has been minimized.

Copy link
@littledan

littledan Mar 15, 2019

Author Member

Thanks for writing this! I added it to the FAQ.

@ljharb

This comment has been minimized.

Copy link
Member

commented Mar 15, 2019

I like the general direction of this change, but I’m concerned about import await being viral. It’d be great if the only thing that makes a module async is adding TLA.

@domenic

This comment has been minimized.

Copy link
Member

commented Mar 15, 2019

I would be opposed to the proposal moving forward with this change. By making callers need to know whether they are importing a module which awaits or not, this essentially makes the proposal sugar for the export default async function / await import() antipattern documented in the readme, and obviates its value.

To be clearer about why it's important for callers to be agnostic, one of the major use cases of top-level await is maintaining the module abstraction boundary while allowing the depended-upon module to add asynchronous initialization steps. @wycats has made this point in previous meetings.

Here is an example. Consider a module like so:

const template = `<p>Hello, my name is: <slot></slot></p>`;

customElements.define("name-tag", class extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' }).innerHTML = template;
  }
});

which is then imported via

import "./name-tag.mjs";

Then the designer gets their hands on things. "This template is too simple!", they say. They beef it up, with a <style> element, a few wrapper divs, maybe some extra flair. The developer decides it'd be best if the designer worked on that in a separate file, name-tag-template.html. They refactor their code to pull in the template from that file, like so:

const template = await (await fetch(new URL("./name-tag-template.html", import.meta.url))).text();

customElements.define("name-tag", class extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' }).innerHTML = template;
  }
});

The point of top-level await is that this is not a breaking change for the consumer of name-tag.mjs. Just like you are able to refactor one module into multiple modules, with top-level await, you can refactor from one module into multiple files, or in general multiple async initialization steps.

With the proposal in the OP, this refactoring becomes a breaking change, for the consumers of name-tag.mjs (recursively, throughout the entire graph).

One way to mitigate this would be if everyone everywhere preemptively replaced all import statements with await import statements. You could see this becoming an ESLint rule, perhaps even a recommended one. It's certainly one we'd enforce on any web projects. But that breaks when cross boundaries between code bases; a package author needs to make a breaking change and broadcast to their users, "any code that imports this package needs to switch their codebase from import to await import." And this of course then propagates, so any indirect dependents also need to update. Etc.

Slowly, gradually, and with much pain, we will either switch the entire JavaScript ecosystem to using await import instead of import, or we will declare that package authors which use top-level await are using a "bad part" of JS, and their packages should not be depended upon, and the feature dies out (except maybe at the top app-level).

Introducing this kind of dynamic into top-level await makes the proposal unusable for me as a package author, and for the cases we are interested in in the web component ecosystem in general.

@littledan

This comment has been minimized.

Copy link
Member Author

commented Mar 15, 2019

@ljharb I'm not sure what you mean. What do you think should happen if you import a module which has a TLA? Are you suggesting semantics like #49?

@littledan

This comment has been minimized.

Copy link
Member Author

commented Mar 15, 2019

@domenic Yes, this does add friction to the upgrade path; that's the idea of this change. There was an exact opposite concern that it would be unfortunate if a deep dependency became async, now all dependent modules are async. Seems like we just have to make a decision one way or the other on whether this transparent upgrade is desirable or not.

@jhnns

This comment has been minimized.

Copy link

commented Mar 16, 2019

@domenic @ljharb that's the idea of this change: using top-level await should be a breaking change for the module consumer. I think this is desired since async operations can take a significant amount of time. If anyone in the module graph was able to defer the application startup, it might become hard for application authors to find the cause for long startup times.

Another advantage of this change is that it would not change the way how module evaluation is currently specified. Introducing TLA to the specification would be a non-breaking change.

@domenic regarding your example: native HTML modules would make the module graph synchronous again.

Slowly, gradually, and with much pain, we will either switch the entire JavaScript ecosystem to using await import instead of import, or we will declare that package authors which use top-level await are using a "bad part" of JS, and their packages should not be depended upon, and the feature dies out (except maybe at the top app-level).

I think, this is a valid concern (and it's probably what @ljharb meant with TLA becoming viral). But it's also very speculative and hard to predict. If common things like HTML and CSS can be imported synchronously, I don't think that this pattern will become too popular. It might be a different story with Web Assembly modules though since synchronous compilation is discouraged by some engine implementers. If WASM modules become popular in the JS ecosystem, import await might become the de-facto standard for importing things in JavaScript.

However, I still like that the application developer is in control to decide whether to import await or to await import things. The explicit import await makes it clear which part of the module graph might take more time to initialize.

@ljharb

This comment has been minimized.

Copy link
Member

commented Mar 16, 2019

@jhnns i have no issue with adding TLA being a breaking change; my issue is with adding import await being one.

@domenic

This comment has been minimized.

Copy link
Member

commented Mar 16, 2019

@jhnns HTML modules will be async, not sync.

@littledan

This comment has been minimized.

Copy link
Member Author

commented Mar 16, 2019

@domenic Good to know that Wasm modules aren't the only one. I can see how it's not so nice that you have to update all the imports to import await if you want to introduce a Wasm or HTML module in your dependency graph.

Do you have any references with more information about HTML modules being async? Maybe we could point to them from the presentation and the explainer here so people can learn more context.

@sokra

This comment has been minimized.

Copy link

commented Mar 17, 2019

The intention of import await was not to rewrite major parts of the imports to import await. Instead it should only used when needed, and the chain should be broken by using import() and non-top-level awaiting it.

This viral behavior is intentional and will make developers aware bad practices.

For web applications you should avoid that your application/entry point becomes an async module, even when other async modules like WASM are used. When your application becomes an async module this probably means the user will see a white screen until all async modules are resolved (i. e. until WASM is compiled). Instead viral import await makes this problem visible and it can be "solved" by breaking the chain with import() and adding spinners or whatever. I agree that there are also some use cases where it might be fine that the entry point becomes an async modules (i. e. Node.js), but for most things it's not good.

Note that the import await syntax didn't introduce the viral behavior. It was already there in the original proposal. Using the syntax only makes it visible to the developer.


This proposal allows `await` to be used at the top-level of a module, outside of functions.

This proposal adds a new type of `import` statement, which is `import await`. `import await` can be used both modules which do and don't contain a top-level `await`. However, `import` statements may not be used on modules with top-level `await`--that misuse causes an error during the Linking phase. Some forms of `import await`:

This comment has been minimized.

Copy link
@trotyl

trotyl Mar 17, 2019

Contributor

import statement

Is ImportDeclaration a Statement?

This comment has been minimized.

Copy link
@littledan

littledan Mar 17, 2019

Author Member

Eh, it's an explainer.

@littledan

This comment has been minimized.

Copy link
Member Author

commented Apr 26, 2019

As presented in the March 2019 TC39 meeting, the champion group does not plan to take this approach, so closing the PR.

@littledan littledan closed this Apr 26, 2019
@littledan littledan referenced this pull request May 23, 2019
0 of 2 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
You can’t perform that action at this time.