
Top-level await in JavaScript is a footgun - rich_harris
https://gist.github.com/Rich-Harris/0b6f317657f5167663b493c722647221
======
Noseshine
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...](https://gist.github.com/Rich-
Harris/41e8ccc755ea232a5e7b88dee118bcf5)

~~~
Bartweiss
This is probably the bigger deal. It means that not only is top-level await
_dangerous_ , it's potentially destructive even when used correctly.

~~~
domenicd
It's also based on yet further misunderstandings (in this case not
understanding the ability of engines to do speculative fetches).

~~~
rich_harris
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.

~~~
domenicd
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.

~~~
rich_harris
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](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?

~~~
domenicd
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.

------
thomasfoster96
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.

~~~
dangoor
ES6 modules are declarative in the sense that the dependencies are statically
analyzable.

~~~
rich_harris
> 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...](https://gist.github.com/Rich-
Harris/41e8ccc755ea232a5e7b88dee118bcf5)

~~~
domenicd
> 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.

------
johnhenry
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?

~~~
rich_harris
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.

~~~
johnhenry
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 :/.

~~~
rimunroe
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.

~~~
Bartweiss
And as we all know, what JS is missing is a new way for unknowing devs to
build slow and buggy products.

------
domenicd
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.)

~~~
rich_harris
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.

~~~
domenicd
> 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
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...](https://gist.github.com/Rich-
Harris/41e8ccc755ea232a5e7b88dee118bcf5)).

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...](https://gist.github.com/Rich-
Harris/9a270920e203e6df9477ca02318fb640)

------
cousin_it
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.

~~~
aikah
> 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
I stand corrected about packages vs modules, good point.

------
domenicd
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.

~~~
scribu
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.

~~~
domenicd
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.

------
jameslk
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.

~~~
rich_harris
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.

~~~
thomasfoster96
> 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
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?

~~~
thomasfoster96
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.

~~~
untog
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?

------
rco8786
> 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?

~~~
rich_harris
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.

~~~
domenicd
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.)

~~~
untog
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.

~~~
domenicd
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.

~~~
untog
Is it a good thing for your entire app to be held up by any component that
needs to load data? I'm sceptical.

~~~
Touche
So you're ok that your app waits on module dependencies, but data dependencies
it should not?

~~~
untog
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.

~~~
Touche
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.

------
Kiro
Am I the only one who is happy using normal callbacks? I think they are the
best representation of the asynchronous flow.

~~~
kolme
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.

~~~
shados
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 :)

~~~
egeozcan
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](https://github.com/tc39/proposal-observable)

~~~
shados
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.

------
mstade
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.

~~~
domenicd
This is false in every modern browser and environment.

~~~
mstade
Really? They break if an exception happens on the await line? What happens if
there's a `unhandledrejection` handler set?

------
xg15
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!

~~~
bmeck
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.

~~~
xg15
I wasn't aware of that restriction. That makes sense. Thanks for the info.

------
halis
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.

~~~
ohitsdom
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().

~~~
aikah
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.

------
z3t4
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 ...

------
jondubois
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.

~~~
bmeck
incorrect. `await` is a reserved word only in async contexts, the UI thread
event loop still runs while `await`ing.

~~~
egeozcan
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.

~~~
BinaryIdiot
In that script, yes. But in JavaScript you can have multiple scripts :)

~~~
egeozcan
> 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.

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

