10 ms·
Top-level await in JavaScript is a footgun
- rco8786 10y ago> 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?
- nolanl 10y agoIt'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.
- domenicd 10y agoThat's false. Module loading and execution are separate stages. The dependent modules would load before this await is ever reached.
- gavinpc 10y agoWait... 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`.
- domenicd 10y agoIt would block dependent module execution, but not dependent module fetching, which is the actual expensive part.
- s_kilk 10y agoIt'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 :)
- domenicd 10y agoIt'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.
- nolanl 10y agoSounds like I misunderstood. Thanks for the correction!
- tribex 10y agoAwait essentially is blocking.
- rich_harris 10y agoAn 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.
- domenicd 10y agoThat'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.)
- untog 10y agoBut 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.
- domenicd 10y agoYes. 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.
- untog 10y agoIs it a good thing for your entire app to be held up by any component that needs to load data? I'm sceptical.
- domenicd 10y agoBut your entire app wouldn't be held up! Only that subgraph.
- rich_harris 10y ago
- giulianob 10y ago`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.
- bmeck 10y ago`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.
- domenicd 10y agoExactly 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.
- Noseshine 10y agoHe 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/41e8ccc755ea232a5e7b88dee118bcf5 https://gist.github.com/Rich-Harris/41e8ccc755ea232a5e7b88de...
- Bartweiss 10y agoThis is probably the bigger deal. It means that not only is top-level await dangerous, it's potentially destructive even when used correctly.
- domenicd 10y agoIt's also based on yet further misunderstandings (in this case not understanding the ability of engines to do speculative fetches).
- rich_harris 10y agoDomenic, 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.
- domenicd 10y agoAnd 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.
- rich_harris 10y agoWhen I tried to ask you about this stuff on Twitter the other day, this was your dismissive response: https://twitter.com/domenic/status/774762508091551745 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?
- thomasfoster96 10y agoI 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.
- dangoor 10y agoES6 modules are declarative in the sense that the dependencies are statically analyzable.
- thomasfoster96 10y agoTrue, 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.
- dangoor 10y ago> Unless loading a file is a bottleneck, which I doubt. Loading a file is a bottleneck in the browser, right?
- thomasfoster96 10y agoRight, 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.
- rich_harris 10y ago> 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/41e8ccc755ea232a5e7b88dee118bcf5 https://gist.github.com/Rich-Harris/41e8ccc755ea232a5e7b88de...
- Kiro 10y agoAm I the only one who is happy using normal callbacks? I think they are the best representation of the asynchronous flow.
- kolme 10y agoWell, 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.
- joshuahutt 10y agoAlso, 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)
- elmigranto 10y agoNo. 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.
- euroclydon 10y agoPlease give an example of a flow mgmt lib.
- elmigranto 10y agoProbably the most popular one is `async` — http://caolan.github.io/async/ http://caolan.github.io/async/.
- tkxxx7 10y agoI really enjoy this one - https://github.com/bigeasy/cadence https://github.com/bigeasy/cadence
- domenicd 10y agoThis 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.)
- rich_harris 10y agoThanks 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.
- domenicd 10y ago> 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.
- rich_harris 10y agoIt'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/41e8ccc755ea232a5e7b88dee118bcf5 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/9a270920e203e6df9477ca02318fb640 https://gist.github.com/Rich-Harris/9a270920e203e6df9477ca02...
- jameslk 10y agoI'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.
- rich_harris 10y agoAgree 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.
- thomasfoster96 10y ago> 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?
- rich_harris 10y agoThe 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?
- halis 10y agoI 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.
- iLoch 10y agoThe 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.
- ohitsdom 10y agoI 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().
- zwieback 10y agoAgreed, 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.
- rapala 10y agoWell, 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.
- aikah 10y agoWhat'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.
- johnhenry 10y agoSo, 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?
- rich_harris 10y agoWhether 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.
- johnhenry 10y agoAh, 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 :/.
- rimunroe 10y agoOn 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.
- Bartweiss 10y agoAnd as we all know, what JS is missing is a new way for unknowing devs to build slow and buggy products.
- mstade 10y agoAlso, 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.
- domenicd 10y agoAnother 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.
- pornel 10y agobut 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).
- domenicd 10y agoYes, 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.
- scribu 10y agoBut 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.
- domenicd 10y agoIt 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.
- xg15 10y agoSlighty 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!
- bmeck 10y agotop 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.
- xg15 10y agoI wasn't aware of that restriction. That makes sense. Thanks for the info.
- z3t4 10y agojust 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 ...
- cousin_it 10y agoI 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.
- aikah 10y ago> 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.
- cousin_it 10y agoI stand corrected about packages vs modules, good point.
- jondubois 10y agoYes, 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.
- bmeck 10y agoincorrect. `await` is a reserved word only in async contexts, the UI thread event loop still runs while `await`ing.
- egeozcan 10y agoIf 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.
- BinaryIdiot 10y agoIn that script, yes. But in JavaScript you can have multiple scripts :)