114 comments

[ 2.5 ms ] story [ 198 ms ] thread
> Even assuming all goes well, you've prevented yourself from doing any other work, like rendering views that don't depend on that data.

Is that right? `await` is non-blocking, no?

It's not so much about blocking as it is about delaying the module waterfall. E.g. a module that did `await new Promise(_ => setTimeout(_, 5000))` at the top would prevent any of its dependents from being loaded for 5 seconds.
That's false. Module loading and execution are separate stages. The dependent modules would load before this await is ever reached.
Wait... so the OP's premise is completely wrong? Doesn't the body of a module have to complete before dependent modules will execute their bodies?

In other words, if you put

    for (let i = 0; i < 10e8; i++)
        // blah
in the body of a module, wouldn't that block dependent modules for the duration of the loop?

Would top-level await be different?

edit The premise, as I understand it, is that the module itself is treated as the "currently-executing method" for the purpose of the `await`.

It would block dependent module execution, but not dependent module fetching, which is the actual expensive part.
It's kinda funny how the semantics of this thing are so non-obvious and nuanced, that it takes so much effort to explain to people how it works :)
It's not that complicated; unfortunately a lot of FUD has been spread about alternate models based on misconceptions, which definitely makes it tricky to explain because you have to overcome that FUD.
Sounds like I misunderstood. Thanks for the correction!
Await essentially is blocking.
An async function is non-blocking. await is blocking within the async function. Top-level await basically boils down to a proposal to wrap your entire app inside an async function.
That's false. It wraps each individual module within an async function. They can each proceed concurrently. (And all of them proceed after any `import`ed modules load.)
But any module with a dependency on a module with top-level await will have to wait for that dependency. As will anything depending on that.

It's not difficult to imagine some code nested deep in some NPM module completely changing the load behaviour of your app without you knowing.

Yes. Let's rephrase that first sentence.

"Any module with a dependency on data that it needs to execute will have to wait for that data before it executes."

That's a good thing.

Is it a good thing for your entire app to be held up by any component that needs to load data? I'm sceptical.
But your entire app wouldn't be held up! Only that subgraph.
So in the future, you'll have to split up your app into multiple entry points specifically to contain the effects of top-level await spilling out? Sounds like using explicitly async functions would be a far more understandable and maintainable approach.
So the argument is that the bad-effects of top-level await are viral. One bad apple in the app entry point's dependency graph stalls evaluation of the main entry point. And to mitigate this risk, you have to segregate out sub-graphs that you safely load via imperative import().

Doesn't the same risk apply if one of the modules in the graph declaratively imports a module hosted on a slow external CDN?

It does, yes. Using a slow, external CDN is a bad idea for exactly that reason.
So you're ok that your app waits on module dependencies, but data dependencies it should not?
But my app doesn't wait on module dependencies - the code is bundled. It does wait on code parsing time (which no, I don't consider ideal) but that's a far cry from waiting on an async data download operation.

As far as I'm concerned, an explicit call to `ModuleB.loadData()` is far better than having it happen as part of module loading, without me necessarily being aware of it. Right now, module loading is assured to be synchronous, and losing that certainty seems like a bad idea to me.

If you're going to perpetually use scripts then this conversation doesn't apply to your. Your script bundles will continue to work as they already do.
`await` will "block" the current method but the current calling method will continue executing unless that method calls await itself (and so on). You can perform whatever other work needs to happen that doesn't require on the data. It's just a matter of having the right architecture in place that doesn't just waits for the request to finish before doing additional work.
`await` allows the event loop/job queue to run while waiting. However, while using `import` declarations, your module will not run until all imports have completed; this means that your module will be suspended while the dependency awaits.

If your app is shipped as a single bundle this means your entry point would be unable to run anything while dependencies are awaiting if you use `import` declarations. If your app places runtime dependent imports into a dynamic import like using `import()` as a function, it would not be suspended while waiting.

Exactly correct. But note that

> your entry point would be unable to run anything while dependencies are awaiting

is very different from the situation with synchronous I/O (in a single-threaded environment).

With sync I/O, not only is your entry point unable to do anything (which is what it chose to do, by using `import` instead of `import()`), your entire app is unable to do anything! So other entry points are also blocked.

Even worse, since sync I/O blocks the event loop, it prevents the user from interacting with your web page or Node app---e.g. scrolling, clicking links, pressing Ctrl+C to exit, and so on.

He posted a follow-up superseding (or extending) the submitted post:

    > A lot of people misunderstood Top-level await is a footgun, including me.
https://gist.github.com/Rich-Harris/41e8ccc755ea232a5e7b88de...
This is probably the bigger deal. It means that not only is top-level await dangerous, it's potentially destructive even when used correctly.
It's also based on yet further misunderstandings (in this case not understanding the ability of engines to do speculative fetches).
Domenic, I do wish you'd at least try to engage fruitfully in these discussions without resorting to your usual snark and condescension.

When you talk about speculative fetches, are you suggesting that engines would guess at which modules were going to be imperatively imported before actually running the code? That doesn't seem like a general solution, or even a particularly desirable one, but I'm interested to learn what you're referring to.

And I wish you'd try to engage with me instead of jumping to your usual tone arguments.

Yes, that is indeed what I am referring to, as engines already do for HTML.

When I tried to ask you about this stuff on Twitter the other day, this was your dismissive response: https://twitter.com/domenic/status/774762508091551745

Speculative fetching is possible in HTML because <link> and <img> and <script> tags etc are declarative. Similarly, modules can be fetched without the code executing because `import` is declarative. How can a browser prefetch modules when it encounters an imperative statement like `x = await import(computedModuleId())` without actually running the code?

Yes, I found all of your points wrong, as I'm explaining in a medium with more than 140 - @-names characters in this Hacker News thread.

See above for a response to your point here.

Can you elaborate at all?

I'm familiar with the concept, but I didn't think that it was standard practice or heading that way for JS. I also have the impression that it comes with serious overhead of its own, making the "await considered harmful" performance claims valid even in that case.

I don't mean that as an argument: if speculative fetches handle this issue I don't understand how, and I'd really like to know.

Speculative fetching is a time-honored technique in web browsers. During the tokenization phase of HTML, CSS, and even JavaScript, they speculatively start a fetch for a given URL, so that if the data needs to be evaluated later, it is ready to go without a network round-trip. (In HTML: script, link, img, video, etc. elements; in CSS: @import declarations; in script: certain common document.write('<script src=..') patterns). Such speculative fetches are low-priority compared to explicit ones.

In the context being discussed here, it means that `await import('./foo.js')` could kick off a speculative fetch for foo.js and all of its dependencies during the tokenization stage---exactly the same as is done for declarative `import './foo.js'`. (Per spec, the fetch for `import './foo.js'` doesn't happen until parsing, which is generally too late to give a good user experience---i.e., this is something engines will already be doing.) You could even imagine extending this to `await fetch('./foo.json')`, although I imagine that will be more of a stretch.

Of course that doesn't help for dynamic cases like `` await import(`./language-packs/${navigator.language}.js`) ``. But that's exactly as you'd expect: if your application truly depends on runtime-determined resources before it can proceed, then of course it needs to avoid continuing to evaluate code that depends on those resources. Top-level await just gives you a way to express that dependency without wrapping the rest of your app in async functions.

> `await import('./foo.js')` could kick off a speculative fetch for foo.js

Interesting, and kind of makes sense.

However:

> Of course that doesn't help for dynamic cases like `` await import(`./language-packs/${navigator.language}.js`) ``.

Isn't that the main, possibly only advantage of `await import(...)` though?

For static import names, why write `const x = await import('x');` if you can simply write `import x from 'x';`?

Because await import() can be used inside if statements or similar.
I know that the argument that "silly developers shouldn't be allowed to do silly things" is a valid one, but I don't see a convincing argument that top level await is worse than what developers are currently doing, which is use synchronous functions to do IO at the top level.

If anything, top level async would be an improvement, as it would give developers no reason to do synchronous IO.

Edit: Additionally, ES6 modules are already imperative. There isn't any way to make them declarative.

ES6 modules are declarative in the sense that the dependencies are statically analyzable.
> If anything, top level async would be an improvement, as it would give developers no reason to do synchronous IO.

Top-level async would make asynchronous IO no different from synchronous IO! It's the worst of both worlds.

> the dependencies are statically analyzable

Exactly – which means modules can be loaded concurrently, without having to execute the code. By contrast, imperative loading has to happen sequentially. I explored this aspect of it in a follow-up: https://gist.github.com/Rich-Harris/41e8ccc755ea232a5e7b88de...

> Top-level async would make asynchronous IO no different from synchronous IO! It's the worst of both worlds.

That's false. You could still do plenty of things while this asynchronous I/O is happening (including allowing the user to interact with the page, or load other modules in the background).

> By contrast, imperative loading has to happen sequentially.

Yes, but notably, declarative loads are not blocked on imperative loads.

> Top-level async would make asynchronous IO no different from synchronous IO! It's the worst of both worlds.

No it's not. Synchronous IO blocks everything. Asynchronous IO with top level await would still allow things queued in the event loop to execute. Plus, top level await + Promise.all would allow you to do operations concurrently, something that synchronous doesn't.

True, but I interpreted the OP as criticising any execution of code on module load (other than declaring exports).

Unless loading a file is a bottleneck, which I doubt.

> Unless loading a file is a bottleneck, which I doubt.

Loading a file is a bottleneck in the browser, right?

Right, but I don't see how top level await makes the current situation any worse. Web developers already have to think about the order of script loading and execution.
Am I the only one who is happy using normal callbacks? I think they are the best representation of the asynchronous flow.
Well, I was happy using normal callbacks, but then promises made me a bit happier and now I even happier with async/await.

You can definitely write clean code with only callbacks, but one has to be pretty careful if there are many nested asynchronous actions.

Promises and await make that code very much clearer.

Also, if you're trying to do something after multiple async operations, you've got to manually count your callbacks. (As opposed to using Promise.all)
And then as you use promise based code more and more (including with async/await), you eventually come to realize that while it looked good at first, its deeply, deeply flawed, and is at best a stepping stone to Observables :)
An observable is semantically very different to a promise, even if it can be thought as a promise with subsequent success calls.

Here is the proposal: https://github.com/tc39/proposal-observable

Your point being?

The semantic is different, but everything a promise can do, an observable can do (with roughly the same amount of code), but the other way around isn't true.

Observables are just better in basically every ways. Real world implementations also have proper error handling and decent APIs to handle aborting -today-, while the TC39 is still jumping back and forth trying to figure out how to handle it in promises so that Fetch can stop being useless.

I don't mind callbacks for async flow, but they involve a huge amount of boilerplate when it comes to error handling. Promises make that a lot better.

I haven't yet switched over to await because of things like the issues described in this post - I don't mind async operations being a little clunky because it highlights where they're actually happening. Only using a simple keyword makes me worry that the flow control won't get as much attention. For me, Promises are a happy medium that allow you do to do simultaneous operations (mapping, Promise.all) a lot more easily than you can with callbacks, but more obtrusively than with await.

(comment deleted)
No you're not. I'm fine with w/ever abstraction, but prefer callbacks and their composition (`async`, events, streams…) over others for simplicity and their behavior matching expectations.
I think "async functions" are peak async handling. That is either a function returns synchronously, or returns a promise. Anything more feels like fiddling.
I'm only using normal callbacks and async javascript iterators . No promises or await.

Easier mental model, no surprises, still easy to organize.

I'm also happy with normal callbacks, and have no plans on adopting promises or async/await. the flow control library "async" provides all the tools to keep code readable.
This is basically the same old argument against `await` itself: that it allows developers to write bad code that should be parallelized more (by using Promise.all etc.).

It didn't stop await inside async functions, and it's not going to stop top-level await.

(The argument also seems to be predicated on some fundamental misunderstandings, in that it thinks everything would have to be sequentialized. See my other replies throughout this thread.)

Thanks for clarifying the point re module evaluation not being sequential – that would seem to break some fundamental guarantees about execution order, but I guess that's a separate discussion. The point stands that your entry point has to wait until each of its dependencies and each of their dependencies (&c) have finished before your app can start, and it unavoidably slows down module loading.

> This is basically the same old argument against `await` itself: that it allows developers to write bad code

It magnifies the effect to a degree that isn't immediately obvious – hence footgun. Users are the ones who will suffer, therefore it's right that we exercise some restraint.

> The point stands that your entry point has to wait until each of its dependencies and each of their dependencies (&c) have finished before your app can start, and it unavoidably slows down module loading.

That's true regardless of whether you use top-level await or not.

It's true that your dependencies have to execute, obviously, but it's much easier to slow things down if you can block while asynchronous work happens. Right now that's not possible.

Meanwhile, you still have to do more work to load the app in the first place (https://gist.github.com/Rich-Harris/41e8ccc755ea232a5e7b88de...).

I'm genuinely curious about the statement that modules can execute concurrently, and what that means for predictability. Jotted down some thoughts here: https://gist.github.com/Rich-Harris/9a270920e203e6df9477ca02...

I'm not terribly familiar with this new proposal but I have used the regular async/await features quite extensively and can't really see a problem here.

Since async/await deal with promises, there's nothing stopping anyone from not awaiting a dependency at the top level so that some loading logic can be run.

In addition, since these are promises, there's nothing stopping us from awaiting multiple promises at once in parallel like so:

    const [dependency1, dependency2] = await Promise.all([promise1, promise2]);
All await does is schedule the resumption of the code following an await later on, so any JS not depending on this blocking dependency can continue to run. That is, await doesn't block the entire execution, unless the rest of the entire app code is sitting behind an await.

I don't even understand why top level await is necessary given HTTP 2 server push. The point is that import statements at the top can be statically analyzed so the web server can determine which dependencies to push down to you with your request. That's not quite here yet, but by the time this proposal is ready I'm sure it will be.

I guess if you need to use runtime evaluation for something to determine you're dependencies it makes sense but I've found that's rarely the case.

Agree that `await` by itself is useful, and your example shows the correct way to use it for concurrent activity. The problem with top-level await is that any modules depending on a module with a TLA cannot execute until it's done – the effects cascade and magnify throughout your app, rather than being confined to an explicitly async function.
> The problem with top-level await is that any modules depending on a module with a TLA cannot execute until it's done – the effects cascade and magnify throughout your app, rather than being confined to an explicitly async function.

Can't I already do this by putting a fs.readFileSync call or a synchronous AJAX call in my code?

The comparison to sync XHR is apt – put it this way, if you could go back in time and prevent synchronous XHR from being a thing, wouldn't you?
Yep, I probably would.

However, the argument against allowing await in top level code doesn't make much sense if all of these bad side effects already exist.

This would just increase the surface area of a bad side effect, though. Barely anyone uses sync XHR and the API is being phased out to the Fetch API - why would we encourage re-adding baggage like that?
See elsewhere in this thread for why the comparison to sync XHR is very misleading.
It seems it would only cascade through the entire app if only the app was designed around dependencies being available first before other code is run. That is, if the app is designed badly.

Again, I don't understand the point of the proposal in this context since nobody should be awaiting a long list of dependencies to load first. If you have 100 dependencies that means you have 100 HTTP requests, which is really a poor design decision. That's why we bundle up dependencies into fewer chunks first before sending them down to the client.

Edit: I understand that this would become a sequential issue if one import leads to a request which leads to further imports which lead to more subsequent requests. My point is that unless the developer(s) really don't understand how async/await works, they wouldn't reasonably design an app like this.

> only the app was designed around dependencies being available first before other code is run

I'm not sure I follow – isn't that the definition of 'dependency'?

> If you have 100 dependencies that means you have 100 HTTP requests, which is really a poor design decision

Agree – and it turns out that's still the case with HTTP2. But bundling doesn't solve the problem of await blocking your app, unfortunately.

Some of the "other code" may not depend on the dependency. I think the word dependency may have been confusing in my statement.

Await doesn't actually block the entire app (or JS thread) like synchronous code does. It schedules the code that follows after an await in the same file or in an async function to run later. You probably already understand this, but my point is that other code outside of this context is free to continue running.

I was never a fan of async/await in C#, I just never found it that useful.

I've seen examples of it in JavaScript and the author is always like, "Look at what you normally have to write and now look at what you COULD write with async and await!"

And it's like the same goddamn code with a few minor differences.

If they axed the whole proposal, I could get through this holiday season without even being depressed a little bit.

The async/await example is much cleaner. I also think it's a great pattern for async code. It allows you to get away from callbacks altogether, and replaces those annoying conventions with "synchronous-like" code, which to me as someone who writes async code daily, is a god-send.

The amount of visual noise in the promise example cannot be understated.

I find async/await in C# to be super useful, and fairly addictive once you start using it. Say you want to do three independent things in a function. How would you run them in parallel without async? Delegates, background workers, or threads? With async, this is simply done with Task.WhenAll().
Agreed, there are situations where it's super useful and I never want to go back to the old way.

I did read up on the paper that explains how the compiler rewrites the code before being comfortable using await.

Well, the runtime then runs the Tasks in a thread pool, so using Task.WhenAll is just a one more item to the list of options in that sense. It is a nice abstraction though.
What's wrong with threads and synchronizing? async I/O has its own level of complexity. But to be frank a better option is go-routines + channels + select construct like with go. That's really what makes both Go and Erlang unique.
You could say this for literally every single feature that got implemented in a language. Eventually a language needs to change and get better and it does it through small incremental changes as ES is doing. Future developers will only use await/async and they will look at old syntax/semantics and have the same attitude towards it that you have now towards async/await.
I'm a fan of async/await in C#, but never use them. I've been deep into the bowels of callback hell and it's lame. Debugging other people's asynchronous state wrangling machines is also lame.

But I just don't write enough asynchronous code to actually benefit that much from async/await, and not all asynchronous patterns map nicely to the hierarchical nature of call graphs for me to take advantage of async/await all that sanely, as I found out from a few attempts to force the issue. And how do I add good debug visualizers to display tasks in flight? How do I serialize out long running async tasks? How do I modify things without breaking that serialization?

You're getting downvoted because I don't believe you made a compelling argument against await / async. I do, however, fall into a similar line of thinking. For instance the code given was already poorly constructed so yes await / async will make poorly conceptualized code look better. But await / async forced an asynchronous behavior into synchronous meanwhile many other aspects of your application will be using asynchronous in the traditional sense. So it essentially mixes the two. This makes the code less maintainable, in my experience, because now you have code that haves the same but is structured in two very, very different ways that look like the traditional, blocking way of structuring code.

Due to this mixing I've seen many mistakes occur using await / async.

Yes because people screw up doesn't mean you don't include / create the feature. But at the same time I find the mixing of the two awkward and confusing at times.

I'm a huge fan of messaging patterns and async / await isn't really useful in a message based system so I don't have much of a dog in this fight.

So, this makes me a little sad because the title makes it seem like top-level await is, in general, a bad thing, but upon closer reading, there only seems to be an issue with using await in conjunction with import -- is this a correct assertion, or am I missing something? I've been making heavy use of await lately, and would love if it were available at the top level, but I have zero intention of using it in the way it's described in the article?
Whether or not it's a module or something else being awaited isn't the problem. The issue that if your app.js (or whatever) has dependencies on modules with top-level awaits, however far removed, it can't execute until they're done. In other words top-level await essentially blocks your entire app.

Using await inside an explicitly async function is great, because it only blocks code inside that function – the effects are localised and much easier to predict/reason about.

Ah, thank you, that makes sense. The term "footgun" is pretty interesting here, as it generally refers to something that allows one to shoot one's self in the foot. In this case, it's allows someone else to shoot you in the foot by hiding it in their code :/.
On the subject of whether or not this is really a problem because people can just be educated not to do this, the author said:

> You'll educate some of them, but not all. If you give people tools like with, eval, and top-level await, they will be misused, with bad consequences for users of the web.

This is still a footgun, because it makes it easy for people to introduce wide ranging and sometimes subtle bugs in their software.

And as we all know, what JS is missing is a new way for unknowing devs to build slow and buggy products.
Now we just need asynchronous imports and all is well again.

I can't even tell whether I'm being sarcastic or not.

Presumably you'll be able to tell at some point in future?
No, he provides a simple example of loading a page depending on a sequence of json operations -- code after the first await is stalled until they all finish.

await import just makes problems worse and, probably, almost unavoidable.

Also, not to forget, `async import` would hide any potential syntax errors or errors in loading, since promises silence exceptions unless explicitly handled. Unless, of course, there's a special exception (no pun intended) to how promises work for imports.
This is false in every modern browser and environment.
Really? They break if an exception happens on the await line? What happens if there's a `unhandledrejection` handler set?
Another misconception here is the lack of understanding of engines' speculative loading capabilities. It's very easy for an engine to notice `await import('./foo.js')` or even `await fetch('./foo.json')` during the tokenization phase, and realize it might be a good idea to go fetch that file (and in the former case, all of its dependencies). This is already done for HTML's tokenization phase (with img, iframe, etc.), and would of course make sense for JS as well.
But if the engine speculatively loads modules, doesn't that counteract the developer's intention to increase performance by loading a module only when needed?

This is not a problem in HTML loading, because HTML doesn't have `if` statements.

It is a problem in HTML, actually, e.g. `<div hidden><img src="foo.png"></div>`. Per spec that fetch should not happen until the div becomes un-hidden. (Which is only determinable at runtime, since you can override the UA stylesheet for div[hidden].) But browsers make the intelligent tradeoff that the image is likely to be used, by doing a low-priority fetch for foo.png.
but the use-case for these is not a static import (that is possible already), but a dynamic one, such as:

    await import(`${config.providerType}.js`); 
and this can't be resolved at tokenization phase (and brute-forcing it with speculative/symbolic execution would be too much of a hack IMHO).
Yes, indeed. If your app actually depends on dynamically-loaded information before it can continue, then of course your app needs to block further evaluation. Top-level await simply allows you to express that without placing the rest of your app inside async functions.
I understand why `await import('./foo.js')` works here, as import() is not a normal function.

But in the case of `await fetch('./foo.json')`, fetch is a global. What happens if my code overwrites window.fetch and redirects ./foo.json to ./bar.json? Won't the engine try to fetch foo.json (which might not exist) when it should wait for the code to execute?

Similarly to the `<div hidden>` example above, it's a tradeoff. A speculative fetch is just that: speculative. It might not always be correct, but on balance, it's likely to improve the user's experience.

I think it's pretty unlikely we'd see this kind of speculative preloading for fetch though, at least initially. More likely, it's done for imports, and then some number of months or years down the line some engineer or PM has the bright idea to measure how much the average web page could be improved by doing speculative preloading for fetch, and then if the measurements are favorable and the technical tradeoffs are manageable, it happens.

Thanks, I wasn't aware that it would accept misses like this; I assumed that 404s would be avoided. Glad to learn something new, thanks!
Slighty off-topic, I'm surprised/amazed that top-level await is permitted at all.

This looks like it would allow some interesting new patterns in web programming, though at the expense of wreaking havoc with traditional execution models.

For example, await in event handlers:

  <button onclick="await doOneThing(); doAnotherThing()">
Is the event object still valid when doAnotherThing is called?

Or await in script blocks:

  <p>Welcome back,
    <script>
     document.write(await fetchUsername());
    </script>
  </p>
Look ma, I stalled the page load with no synchronous XHR!
top level await is only possible in grammar w/ `await` as a reserved word. This is limited to Modules (type=module) and async functions. You can't put it in your click handler or random Script like the examples you gave.
I wasn't aware of that restriction. That makes sense. Thanks for the info.
just make your http2 server read "package.json" and push the dependencies to the browser. Could even use unique module url so popular modules would already be cashed ...
I agree that top-level await is weird. But the bigger issue is that importing stuff shouldn't be an imperative statement ("load the code at such-and-such address"). That's marrying the language to irrelevant details of how it's parsed and executed. Instead, import should be a static construct that can get special treatment from interpreters and compilers. Header files in C are another instance of the same mistake ("let's make imports work by textual inclusion"). Such tricks can save time in the short run, but a proper module system like Java's is always better in the end.
> but a proper module system like Java's is always

Java has a package system not a module system.The difference is important because Java might introduce a module system in the future. And Java isn't built around async programming by default, even loading a jar is synchronous.

I stand corrected about packages vs modules, good point.
Yes, top-level await sounds silly. It would freeze the entire app... You couldn't even render a loading progress bar - It would defeat the whole point of loading modules asynchronously.
incorrect. `await` is a reserved word only in async contexts, the UI thread event loop still runs while `await`ing.
If you run an await as a first thing in the top-level, you have nothing in the event loop to run. Await blocks everything after itself, and that is very dangerous if done on the top-level.
In that script, yes. But in JavaScript you can have multiple scripts :)
> in JavaScript you can have multiple scripts

You mean, you can embed multiple script elements in HTML? Yes. But allowing global await in ES spec means also enabling it in node.

or you could just do `import('a'); import('b');` to load `a` and `b` in parallel