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.
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.
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.)
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?
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.
`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.
> 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.
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.
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?
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.
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.
> 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.
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.
No. You use flow management library. Just how you don't manually execute multiple promises with `Array.forEach` and same manual counting. Latter just happens to be in stdlib while former is userland.
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 :)
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.
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 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.
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:
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?
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?
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.
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().
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.
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.
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.
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.
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.
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.
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.
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.
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.
114 comments
[ 2.5 ms ] story [ 198 ms ] threadIs that right? `await` is non-blocking, no?
In other words, if you put
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's not difficult to imagine some code nested deep in some NPM module completely changing the load behaviour of your app without you knowing.
"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.
Doesn't the same risk apply if one of the modules in the graph declaratively imports a module hosted on a slow external CDN?
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 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.
> 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.
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.
Yes, that is indeed what I am referring to, as engines already do for HTML.
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?
See above for a response to your point here.
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.
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.
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';`?
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.
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...
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.
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.
Unless loading a file is a bottleneck, which I doubt.
Loading a file is a bottleneck in the browser, right?
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.
Here is the proposal: https://github.com/tc39/proposal-observable
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 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.
Easier mental model, no surprises, still easy to organize.
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.)
> 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.
That's true regardless of whether you use top-level await or not.
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...
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:
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.
Can't I already do this by putting a fs.readFileSync call or a synchronous AJAX call in my code?
However, the argument against allowing await in top level code doesn't make much sense if all of these bad side effects already exist.
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.
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.
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'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 amount of visual noise in the promise example cannot be understated.
I did read up on the paper that explains how the compiler rewrites the code before being comfortable using await.
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?
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.
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.
> 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.
I can't even tell whether I'm being sarcastic or not.
await import just makes problems worse and, probably, almost unavoidable.
This is not a problem in HTML loading, because HTML doesn't have `if` statements.
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?
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.
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:
Is the event object still valid when doAnotherThing is called?Or await in script blocks:
Look ma, I stalled the page load with no synchronous XHR!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.
You mean, you can embed multiple script elements in HTML? Yes. But allowing global await in ES spec means also enabling it in node.