
V8 adds support for top-level await - hayd
https://chromium.googlesource.com/v8/v8.git/+/0ceee9ad28c21bc4971fb237cf87eb742fc787b8%5E%21/
======
londons_explore
All this async code without decent locking primitives is leading to a rabbit
hole of race conditions...

It doesn't matter that it's all single threaded if all your function calls may
or may not block and run a bunch of other code in the meantime, mutating all
kinds of state.

I feel like JavaScript developers of the 2020's are going to relearn the same
things the C programmers of the 1990's learn, just a few levels of abstraction
higher.

~~~
_bxg1
_Data races_ are impossible in JavaScript because it's single-threaded. _Race
conditions_ are possible in any language that can do anything asynchronous,
which is basically all of them. But the general benefit you get from JS being
single-threaded is the fact that any given callback is transactional. No other
code will ever come in and mutate state between two regular lines of
JavaScript code. Achieving this is pretty much the whole point of locking
mechanisms, so under normal circumstances they aren't necessary for JS.

That said, when you use async/await instead of promises or callbacks, things
do change because two sequential lines of code are no longer two truly
sequential instructions. You're muddying the most fundamental metaphor of code
syntax. That's why I personally don't like async/await syntax, although I get
why people want it.

~~~
tudorconstantin
Since an async function is simply a function that returns a promise is there
actually any difference between using async/await and using promises
explicitly?

~~~
dboreham
It takes longer to boil your brain.

------
miguelmota
This is huge! Finally no more need to use IIFE's for top level awaits

~~~
megous
It's nice, I guess, but huge?

Instead of:

    
    
        async function main() {
           // code
        }
        main().catch(console.error);
    

I'll be maybe writing:

    
    
        try {
          // code
        } catch (ex) {
          console.error(ex);
        }
    

Hrm?

~~~
Scooty
Top level await does more than remove a main function. If you import modules
that use top level await, they will be resolved before the imports finish.

To me this is most important in node where it's not uncommon to do async
operations during initialization. Currently you either have to export a
promise or an async function.

~~~
tiglionabbit
Do we really want slow imports though? If you have a bunch of modules with
async setup functions, would you not be able to Promise.all() them?

~~~
pandapower2
Sure, in many situations, but I guess if you need to setup things have have
dependencies (init A then init B then init C) this will help.

~~~
dvlsg
How so? I'm not seeing the benefit over exporting A, B, and C as functions,
and then putting them together in another spot (like a composite root for pure
DI, or an IoC container, etc).

Is the argument for top-level await that you don't need the other spot?
Because I feel like you still do - except now it's implicitly inside not only
A, but likely B and C as well to some extent. And in a very inflexible way.

------
bandrami
So COMEFROM is now a first-class feature of the most popular programming
language in the world. Intercal really was ahead of its time.

~~~
bzbarsky
I'm not sure I quite see the analogy to COMEFROM.

The "problem" with COMEFROM is that at the "target" location (the one control
comes from) there is no in-source indication of the control flow transfer. So
what looks like linear code turns out to have this unexpected detour to the
location of the COMEFROM instruction. This hinders understandability of the
code.

"await" in JS doesn't have that problem: there is an explicit control flow
operation, in the form of resolving a promise, that eventually transfers
control to the location of the "await" call. And even then, it's async from a
run-to-completion perspective, so doesn't affect linear code, modulo other
await calls. I guess the concern is that you could have linear code that calls
a function, which does an "await" and you would effectively have a control
flow detour that's hidden from view? In that sense, I guess this is sort of
like COMEFROM... At least you have to explicitly opt in (via "async function",
or async module) for it to be a problem.

Anyway, a better analogy from my point of view is that async/await is a very
limited form of call-with-current-continuation or so. And with generator
functions, JS already had that sort of, but without some of the nice
ergonomics.

~~~
bandrami
> I guess the concern is that you could have linear code that calls a
> function, which does an "await" and you would effectively have a control
> flow detour that's hidden from view?

That's the main issue. The overall control flow cannot be known until the
entire abstract syntax tree is generated.

~~~
bzbarsky
To be fair, that's a problem with GOTO as well (which JS doesn't have, yes);
you don't have to go all the way to COMEFROM to get that....

That said, with JS you can't know the overall control flow even one you have
the AST, because that `foo()` function call could go anywhere depending on
what people did to the global scope independently of your AST.

Come to think of it, you can't even determine control flow from the AST in C,
unless everything involved has static linkage...

------
mstade
I think async/await probably makes more sense in a typed language, where a
compiler can tell you when you're missing an await, or at least warn you about
not dealing with potential side effects and error handling. For something like
JavaScript, it'd make more sense to me to have the runtime always and
implicitly await the result of async functions, and instead make developers
explicitly say when they wish for the result to be async. For example, instead
of:

    
    
        const data = await fetch()
        push(someData) // async, runs in the background
    

You would do:

    
    
        const data = fetch() // Runtime detects promise and awaits result
        async push(data) // the async keyword would return a promise and execute the push function asynchronously, allowing the next line to execute
    

In this fantasy world the "await" keyword would work anywhere and as you'd
expect – awaiting the result of any promise:

    
    
        const data = fetch() // implicitly await
        await async push(data) // this would also be "synchronous" in that it suspends execution of subsequent code until `push` fulfills or rejects the promise, and so it'd have the same effect as implicit await
    

Point is you'd probably await that promise elsewhere, so you'd actually store
away the return value of the `async` call and later on you'd `await`, or
another example would be to await a block.

Promise rejections in implicit awaits would halt execution, just like a sync
function throwing an error, so you wouldn't "miss" an error somewhere because
runtimes swallow promise rejections. (Well, at least Node wised up
eventually.)

This means there'd be no difference in function declaration between sync and
async functions, it'd be determined by whether they return a promise or not
which I _think_ should be possible to statically determine by a JIT compiler
in most cases, so not adding too much (if any) overhead.

Kind of a half baked thought, but point is I always felt the async/await thing
was kind of backwards in JavaScript.

~~~
laughinghan
I like your idea, but I don't see how it could work in an untyped language.
Consider:

    
    
        function foo() { return 1; }
        function bar() { return fetch('http://example.com'); } // implicitly async
        
        function qux() {
          const fn = Math.random() > 1/2 ? foo : bar;
          fn();
          return 1;
        }
    

Is qux() synchronous?

~~~
anonytrary
The function is both synchronous and asynchronous until it is called by the
caller. This is called Shannon's Cat.

Jokes aside, I'd like to add that just because a function returns a promise
doesn't mean the caller will always want to wait for it to resolve. I think of
`await` as a simple modifier that casts the return value from a `Promise<T>`
into `T`. By getting rid of the modifier, we can no longer assume that the
caller wants to implicitly wait.

~~~
mstade
You're right of course, and I have zero data to back this up other than
anecdotes from my own experience, but still I posit the most common case is
that the caller wants to await the return value, not run the function
asynchronously. This is why I think it'd make more sense to flip the semantics
so you'd have implicit await, and have to explicitly mark the things you want
to run asynchronously.

~~~
eyelidlessness
Implicit await would be a nightmare. You could never tell looking at a given
block of code whether it yields to the event loop or not, introducing
invisible concurrency problems. Even static types wouldn't solve this without
looking at every value and function return type.

~~~
mstade
Would love to learn what concurrency issues you'd see with implicit await,
that you presumably wouldn't see otherwise. You're probably right, I just
can't think of any examples.

------
tedivm
Now if only python would do the same.

~~~
xtreak29
Top level awaits are allowed with a compiler flag now in Python 3.8 :
[https://bugs.python.org/issue34616](https://bugs.python.org/issue34616)

python -m asyncio

Starts a new repl with the compiler flag to explore top level await in a repl

------
galaxyLogic
Here's a counter-argument:

[https://gist.github.com/Rich-
Harris/0b6f317657f5167663b493c7...](https://gist.github.com/Rich-
Harris/0b6f317657f5167663b493c722647221)

~~~
hayd
Was any of these concerns allayed?

------
lacampbell
Is this part of the standard?

~~~
mstade
It's a stage 3 proposal[1], which according to the TC39 process[2] means "the
solution is complete and no further work is possible without implementation
experience, significant usage and external feedback."

In other words, it's all but standardized. Barring significant blockers coming
from actual implementation experience this will most likely be ratified.

[1]: [https://github.com/tc39/proposal-top-level-
await](https://github.com/tc39/proposal-top-level-await)

[2]: [https://tc39.es/process-document/](https://tc39.es/process-document/)

~~~
Klathmon
And the way the TC39 works, it won't progress until multiple vendors can
implement it.

Being implemented by multiple js engines is how JS features become
standardized.

~~~
mstade
Yup, thanks for adding that. It's a good process I think, though it is kind of
a double edged sword. On the one hand it's nice because it means the standard
isn't bloated with a bunch of stuff that no one implements, but on the other
hand it also means it's much harder to get rid of stuff that turns out to
maybe not be such a great idea after all, leading to bloat anyway.

Still, it's a pretty good process I think.

------
Footkerchief
Finally JS finishes reinventing the benefits of imperative programming.

------
craftoman
I was waiting years for this. From now on code will be much more readable and
cleaner without all those IIFEs.

------
self_awareness
This looks like Dart's handling of async/await.

------
1wheel
Awesome! How long before this shows up in canary?

------
The_rationalist
Does rust async support this?

~~~
pitaj
Rust async is just syntax: there is no event loop built into the language.

------
thatguyagain
[removed]

~~~
jhanschoo
Nothing. It's just that you can have

    
    
        const result = await someFetchOrSomething(str);
        return result.foo;
    

while not inside an async function. You still can't use it in a non-async
function.

~~~
efdee
You can't return when you're not in a function ;-)

