top | item 21064911

V8 adds support for top-level await

391 points| hayd | 6 years ago |chromium.googlesource.com

295 comments

order
[+] londons_explore|6 years ago|reply
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|6 years ago|reply
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.

[+] jnbiche|6 years ago|reply
Javascript devs, for all their flaws, understand async programming far, far better than the average C programmer.

Indeed, your assertion that we need "locking primitives" to counteract "race conditions" is evidence of that. Yes, JavaScript can have race conditions, but not multi-threaded race conditions that cause resource contention [0].

So what good would locking primitives be?

And as a solution to single-threaded race conditions, it's becoming more and more common to use pure functions and immutable data structures in Javascript. In the ReactJS world, it's practically standard to use immutable data structures.

Furthermore, JS devs know how to structure their code, either using nested callbacks or promises, or now, async/await, to avoid async data races. Anyone who programs primarily asynchronously understands these things.

So in summary, it's pretty arrogant to assume that JS devs are going to have to "re-learn" the same things that C programmers learned in the 1990s (also ignorant, because 1990s C was decidedly synchronous). The only devs I know that struggle with race conditions in Javascript are those who are coming from another language or paradigm and who fundamentally fail to understand asynchronous programming.

0. The only exception to this would be those NodeJS devs who use multiple processes or browser devs using web workers along with the ultra-new and not very well-supported shared (memory) resources, both of which are rare in the JS world because there's not much need. You can achieve adequate performance off of one thread for practically any IO-bound application unless you're operating at Facebook scale. And in those case when people are using multiple processes for CPU bound algorithms, they're almost always using them with async message queues anyway, which obviates the need for any locking primitives.

Edit: https://news.ycombinator.com/item?id=21065831 in this case, async/await can introduce a problem. But using an immutable data structure reference as would almost always eliminate this issue.

Also, in practice you're probably using a database whose library has transactions, so you'd "lock" the transaction in this way.

But OK, if you use async/await or generators along with mutability, then a locking primitive could be useful, I'll concede. Although in a single-threaded program, a boolean is just as good.

[+] random314|6 years ago|reply
I am not sure how the parent comment has received so many upvotes. OP has some fundamental misunderstanding of mutexes and the purpose of async io.

Locking primitives are completely unnecessary in any single threaded program. Also mutexes in C or otherwise are way older than 1990. They go back to the 1950s.

When your program is single threaded, a simple boolean flag variable can act as a mutex, you don't need a mutex primitive for this. Mutexes are for situations where flag variables can change state between your instructions to check and set a flag. This can only happen in multithreaded or interrupt driven code.

Infact, the entire purpose of async programming is to stop the use of mutexes and multiple threads to perform concurrent io, which is something that can be performed by a single thread. This is the foundational premise of nodejs.

[+] drevil-v2|6 years ago|reply
I think JS devs usually use async/await for network calls and not really for computation or file access. Those type of applications are better served by other languages.

A deadlock in the problem space that JavaScript operates in would be a rarity I feel.

[+] james-mcelwain|6 years ago|reply
Despite other deficiencies, JavaScript devs are generally pretty good at writing async code, for the simple reason that you get burned suuuper quickly if you try to mutate state outside of an async context when doing things like adding click handlers. This is something most front-end devs learn pretty quickly since it's such a fundamental part of writing JavaScript.
[+] rictic|6 years ago|reply
I don't know if you intended to, but it comes across as arrogant and dismissive.

Dealing with asynchrony has been the an issue in JavaScript from the beginning, as the fundamental model is event driven.

There are a number of mechanisms for coordinating work, including mutexes (see Atomics.wait()), but for >99% of code, Promises are a better option.

[+] anderskaseorg|6 years ago|reply
Since it’s all single-threaded, you can write your own locking primitives and they will work correctly.
[+] pseudoramble|6 years ago|reply
> 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.

Can you elaborate a bit more on this? I'm unsure about how locking in a single-threaded environment would work. And how would it really differ from async/await or promises which can handle race conditions already?

One area where the lack-of locking primitives does worry me is the use of shared mutable memory. I've never used these techniques in JS, but if I remember right one can now do something like this:

1. Create a shared memory array

2. Spawn several workers which do work on shared arrays

3. Pass a reference to the array to each worker and let them run.

This seems like an area where we could start to see these problems start appearing. There's a module related to atomic operations [1] but there is no language-level enforcement of using it. There could be good reasons why this isn't worrying but I just don't happen to know that off hand.

[1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

[+] Izkata|6 years ago|reply
Oh, you're going to love (hate) what I suspect will happen then:

> race conditions

I've gotten into multiple arguments with first-language-javascript devs who think race conditions can't happen in javascript "because it's not multithreaded". I'm thinking before they can accept this type of bug, it's going to get a new trendy name first, then all the old knowledge of "race conditions" continue to be ignored.

[+] minitech|6 years ago|reply
> All this async code without decent locking primitives is leading to a rabbit hole of race conditions...

I haven’t seen this happening. Maybe it’s because I tend to use Node for web apps too much, which don’t have much shared state within Node to begin with? That it’s single-threaded does matter immensely, though, because if you have shared state that can be updated synchronously, you just write the code and know that it runs as a unit.

[+] wayneftw|6 years ago|reply
In a single-threaded universe, what more do you need to lock the world than a boolean variable?
[+] duxup|6 years ago|reply
Isn't every new language doomed to relearn everything in some way?
[+] masklinn|6 years ago|reply
> All this async code without decent locking primitives is leading to a rabbit hole of race conditions...

That was an issue long before async/await was a thing.

[+] colordrops|6 years ago|reply
Plenty of people replied to counter this misinformation, so why is it still at the top?
[+] namelosw|6 years ago|reply
No, the abstraction is much better than C in 90s.

It comes from functional programming concepts (namely monadic design - which makes Haskell the 'best imperative language') which has some nice algebraic properties. It's easy to reason about, and don't really need lock machanism above the abstraction if properly used.

[+] pjmlp|6 years ago|reply
Have C programmers already learned how to do memory management properly? Apparently 50 years have not been enough.

Don't assume all programming languages have the same design flaws as C.

[+] winrid|6 years ago|reply
A good editor warns about some of those conditions at least.
[+] trust07007707|6 years ago|reply
It's best to use the phrase 'async control flow' instead of 'race conditions' when you're talking about single threaded execution.
[+] miguelmota|6 years ago|reply
This is huge! Finally no more need to use IIFE's for top level awaits
[+] megous|6 years ago|reply
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?
[+] nilkanthjp|6 years ago|reply
Seems convenient. Though I've never thought of top-level async as the killer feature I've been waiting for out of V8. Unless I'm missing something about what this enables...
[+] paulddraper|6 years ago|reply
Though it goes even beyond that.

It not only works for the main entrypoint module, but all other modules as well.

Though I can say I've ever encountered a reason to have that....

[+] bandrami|6 years ago|reply
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|6 years ago|reply
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.

[+] nickitolas|6 years ago|reply
Could you elaborate?
[+] pjmlp|6 years ago|reply
Just like Lisp and Algol 68 on other domains, apparently good features tend to take time to become mainstream.
[+] mstade|6 years ago|reply
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.

[+] minitech|6 years ago|reply
Promises are nice in JavaScript because they’re just a value with no magic. Anything can create them, not just async functions. Generic/higher-order functions and so on can get involved without needing to know the difference. A proposal to introduce magic at calls sounds really awful, sorry.
[+] laughinghan|6 years ago|reply
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?
[+] Scooty|6 years ago|reply
Making await implicit would make it difficult to manage parallel promises. You would either have to make an exception for Promise.all or add new syntax. It's also not unheard of to have hanging promises (fire and forget).

Types make it easier to catch mistakes early in general. Typescript has a compiler rule 'no-hanging-promises' to help avoid forgetting to await.

[+] tedivm|6 years ago|reply
Now if only python would do the same.
[+] xtreak29|6 years ago|reply
Top level awaits are allowed with a compiler flag now in Python 3.8 : https://bugs.python.org/issue34616

python -m asyncio

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

[+] iikoolpp|6 years ago|reply
Impossible due to how async is implemented (as generators).
[+] lacampbell|6 years ago|reply
Is this part of the standard?
[+] mstade|6 years ago|reply
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

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

[+] Footkerchief|6 years ago|reply
Finally JS finishes reinventing the benefits of imperative programming.
[+] craftoman|6 years ago|reply
I was waiting years for this. From now on code will be much more readable and cleaner without all those IIFEs.
[+] 1wheel|6 years ago|reply
Awesome! How long before this shows up in canary?
[+] The_rationalist|6 years ago|reply
Does rust async support this?
[+] pitaj|6 years ago|reply
Rust async is just syntax: there is no event loop built into the language.