@wlib (Daniel Ethridge) posted a new topic, subject "Concurrent Async and Normal Await Evaluation Blocks". It read: ## Summary With promises, concurrency is already trivial to implement, but not in a syntactically clear way. I propose `async`/`await` blocks to alleviate this ergonomic disincentive. ## Motivation If we have a variable dependency graph for `d(a, b, c.property)`: [code] a b c | | | | | (c => c.property) \ | / d [/code] We can implement it intuitively like this: [code] const a = getA() const b = getB() const c = getC() d(a, b, c.property) [/code] But what about when `a`, `b`, and `c` are promises? It could be translated directly from above like this: [code] const a = await getA() const b = await getB() const c = await getC() d(a, b, c.property) [/code] But if we optimally implement this for the given dependency graph, we get this: [code] const _a = getA() const _b = getB() const _c = getC() const a = await _a const b = await _b const c = await _c d(a, b, c.property) // Or const [a, b, c] = await Promise.all([ getA(), getB(), getC() ]) d(a, b, c.property) [/code] This is obviously enough of an obstacle to make people accidentally or carelessly serialize their concurrent code. ## Proposal ### `async` block We need some syntax sugar here for `Promise.all` \- and this is where I want some help with considerations. I think the most obvious solution is this: [code] async { const a = getA() const b = getB() const c = getC() } d(a, b, c.property) [/code] Here, we have an "`async` block" that is unlike other blocks in that it is actually maintaining its scope. It evaluates its statements in a way that is only different in that it automatically `await`'s all expressions that are to be "called by value". An implementation might build a dependency graph of values, with the end of the `async` block being the end of the graph. Then, the graph is just reduced as if each node was independently `await`'ed. In essence, this is a way of opting into lazy evaluation. ### `await` block The complement to the `async` block would be normal evaluation. This can be opted-into with an `await` block, which is most convenient when applied within an `async` block. Here is an example of when it might make sense: [code] async { const resultA = concurrentProcessA() concurrentProcessB() await { await concurrentProcessCStep1() await concurrentProcessCStep2() await concurrentProcessCStep3() console.log("The normal evaluation order we are used to") } concurrentProcessD() } [/code] ## Discussion Now, the scope of this proposal makes it fairly powerful, one may argue that this would completely change the language. If this change is seen as too much, I would happily settle for just a nice syntax sugar for `Promise.all` Then @theScottyJam (Scotty Jamison) said Here's a somewhat related thread. It was trying to solve a different problem, but a lot of the conversation revolved around different syntaxes for `Promise.all()` Then @wlib (Daniel Ethridge) said That was a great thread to read through as it began to overlap more and more with this - thanks! I think that we need, overall, better concurrency handling in the language. I'm sure my proposal would result in a lot of changes in the cores of js implementations, similar to the introduction of async/await. Even with that cost, I think, especially after reading that, that an opt-in "lazy evaluation" mode would be a really valuable evolution of the language. After which, @aclaymore (Ashley Claymore) added wlib: > If this change is seen as too much, I would happily settle for just a nice > syntax sugar for `Promise.all` Perhaps this proposal would interest you: GitHub - tc39/proposal-await.ops: Introduce await.all / await.race / await.allSettled / await.any to simplify the usage of Promises [code] const [a, b, c] = await.all [getA(), getB(), getC()]; [/code] To which @theScottyJam (Scotty Jamison) replied So, with your current proposal, how do you propose something like this works? [code] async { const myArray = [] myArray.push(asyncFn1()) myArray.push(asyncFn2()) } [/code] Is myArray allowed to have `[<result of asyncFn2()>, <result of asyncFn1()>]` when this is done executing? Why or why not? Then @Jonas_Wilms (Jonas Wilms) said I don't like this kind of magic. Reasoning about async code is already hard, having the language do the magic for you will make folks forget what happens under the hood, until it does not work as expected. I see the same problem with Automatic Semicolon Insertion. > Here, we have an "`async` block" that is unlike other blocks in that it is actually maintaining its scope. With `let` and `const` we finally had consistent block scoping in JS ... Just to break that now again? > If this change is seen as too much, I would happily settle for just a nice > syntax sugar for `Promise.all` Jup, that sounds like a good idea ... To which @theScottyJam (Scotty Jamison) replied I'm leaning towards a `Promise.all()` syntax too - I don't see a feasible way to implement a dependency-graph magic solution, in a way where it's easy to understand the order in which things will execute (though maybe I'm just not seeing it). The only question is - what would a `Promise.all()` syntax look like? I think the biggest issue with `Promise.all()` is the fact that the assignment targets are so disconnected from the promises. e.g. [code] const [var1, var2, var3] = await Promise.all([ getA(), getB().then(x => x.y), getC(), doSomethingElse() ]) [/code] The fact that getC()'s result will eventually get assigned to var3 makes this code a little difficult to follow. So here's one possibility, just to throw something out there. [code] parallel const var1 = await getA(), var2 = await getB().then(x => x.y), var3 = await getC(), _ = doSomethingElse() [/code] `parallel const` will cause all of its declarations to be executed in parallel. The "_" is a throwaway variable, for any expressions that don't need their result assigned to something. Any thoughts? Or different ideas? So @lightmare replied I have an idea :) And since I'm in the process of getting myself familiar with Babel internals, I've been working on a proof-of-concept parser patch and plugin. Not sure I'll be able to turn that into an actual proposal, but I may try if there's interest. Instead of an async block as the OP, which is confusing to me because it looks like a scope but the variables bleed out, I declare individual async variables, and wrap every access in await: [code] async function work() { async var a = afoo(); async let b = asleep(); async const c = alas(); future d = awake(); // short for `async const` b = morework(b, c); return a.concat(b, d); } [/code] transpiles to: [code] async function work() { var a = afoo(); let b = asleep(); const c = alas(); const d = awake(); b = morework(await b, await c); return (await a).concat(await b, await d); } [/code] Because I think `async const` is too long yet the most useful, I made `future` a contextual keyword equivalent to `async const`. Then @theScottyJam (Scotty Jamison) said Interesting idea. I'm not sure yet how I feel about variable access meaning different things depending on how it was defined - I'll have to set on this idea for a bit and see how it settles. I'm especially not crazy about the idea of being able to reassign to these special variables and having it preserve its specialness, without any marker at the reassignment site that this is happening - I think if this idea goes forwards, I would only want an `async const` (or `future`) version. This proposal will also have some issues in node. In the transpiled code, if d rejects while we're still awaiting b, then node will stop the process because the promise was never awaited, and never had a catch handler attached (it didn't know we were planning on eventually awaiting it). But, there's ways to resolve that with how it's transpiled, it'll just be something to keep in mind. ...but this is actually an interesting point. Does this mean an exception can be thrown at the location of a variable access, because behind the scenes we're really implicitly awaiting a promise there? And here's one potential oddity I can think of. [code] future x = someAsyncTask() // ... const f = () => x console.log(f()) [/code] I assume that these async variables can only be used inside of async functions, so that will just result in an early error. @lightmare replied theScottyJam: > I'm not sure yet how I feel about variable access meaning different things depending on how it was defined It's mere syntax sugar. Instead of typing await where you need the result, you type async where you're starting the computation. theScottyJam: > reassign to these special variables and having it preserve its specialness Preserving the specialness is the only sane option, if reassignment is allowed. Imagine conditionals. But yes, `future` was my initial idea, however AFAICT nobody's too eager to add new keywords, so I came with the alternative `async` prefix. theScottyJam: > This proposal will also have some issues in node. In the transpiled code, if d rejects while we're still awaiting b, then node will stop the process because the promise was never awaited, and never had a catch handler attached (it didn't know we were planning on eventually awaiting it). But, there's ways to resolve that with how it's transpiled, it'll just be something to keep in mind. Sounds great, that would mean it does more useful stuff than swapping one keyword for another. theScottyJam: > Does this mean an exception can be thrown at the location of a variable access Yes. That's not new: `{ console.log(x); let x; }` That last point is correct: you basically have `await x` in sync function. After which, @theScottyJam (Scotty Jamison) added lightmare: > Yes. That's not new: `{ console.log(x); let x; }` Yeah, but doing that is a straight-up bug, while these thrown exceptions might make sense to catch. So, code like this might become prevalent. [code] async function main() { future fileContents = readFile() future metadata = getMetadata() try { fileContents } catch (err) { if (err.code === 'FILE_NOT_FOUND') return null throw err } return { fileContents, metadata } } [/code] I'm not saying this is necessarily a bad thing, but it is different. Then @theScottyJam (Scotty Jamison) said I've thought of another scenario that `Promise.all()` doesn't handle very well, and neither of our proposed syntaxes will handle it well also. It would be nice if whatever syntax we come up with could also handle this scenario too. [code] // Sync version const getData = async () => ({ name: getName(), birthday: getBirthday(), metadata: { id: getId() }, }) // Async version async function getData() { const [name, birthday, id] = await Promise.all([ getName(), getBirthday(), getId(), ]) return { name, birthday, metadata: { id }, } } [/code] **Edit:** I think I figured out one potential syntax that could help with both use cases. We introduce an `await.multi` keyword. When executing an expression (or statement), and an `await.multi` gets hit, other parts of the expression will continue to execute, potentially causing more `await.multi`s to be hit. Once everything in the expression has been evaluated that is able to be evaluated, execution will pause until all promises have been resolved. Some examples: [code] const getData = async () => ({ name: await.multi getName(), birthday: await.multi getBirthday(), metadata: { id: await.multi getId() }, }) const a = await.multi getA(), b = await.multi getB(), c = await.multi getC() [/code] Note that nested `await.multi` won't be allowed, so you can't do this: [code] const result = await.multi f(await.multi g()) [/code] **Edit 2:** I'm realizing that `await.multi` doesn't make sense when bounded by most statements. It may just be that expressions and declarations are the only things that bound `await.multi`, and no other statement can be used to bound its effect. (By "bound it", I'm talking about figuring out figuring out which `await.multi`s get grouped together into a single `Promise.all()` effect) So @lightmare replied I don't think `await.multi` is viable. Allowing it on arbitrary expressions would wreak havoc in evaluation order. [code] r = [ x(await.multi f()), y(), z(await.multi g()) ]; [/code] How should function calls be ordered? * immediately call what you can in source order, then resolve the rest in source order: * `f, y, g, await.all, x, z` * `f, y, g, await f, x, await g, z` * start `await.multi` first, then resolve everything in source order: * `f, g, await.all, x, y, z` * `f, g, await f, x, y, await g, z` * start `await.multi` first, then resolve the rest as arguments become available: * `f, g, y, ... non-deterministic` Moreover, `await.multi` cannot span across logical operators, conditional or comma operator: [code] await.multi f(a) && await.multi f(b); // should not start f(b) unless f(a) resolves to truthy value await.multi f(t) ? await.multi f(a) : await.multi f(b); // must first resolve f(t), then start either f(a) or f(b), not both a = await.multi f(b), b = await.multi f(a); // yuck [/code] theScottyJam: > Note that nested `await.multi` won't be allowed, so you can't do this: > > `const result = await.multi f(await.multi g())` I don't see a problem here, there is clear topological order in which this can be resolved. To which @theScottyJam (Scotty Jamison) replied lightmare: > How should function calls be ordered? This one is the answer: lightmare: > f, g, await.all, x, y, z I think there isn't an issue as long as we define a clear, deterministic, and predictable ordering to it all, but you're right that this having a different execution order is probably the number one issue with this idea. But, here's how a clump of `await.multi` transpiles: [code] const getData = async () => ({ name: await.multi getName(), birthday: await.multi getBirthday(), metadata: { id: await.multi getId() }, }) // is the same as const getData = async () => { const [$temp1, $temp2, $temp3] = await Promise.all([ getName(), getBirthday(), getId(), ]) return { name: $temp1, birthday: $temp2, metadata: { id: $temp3 }, } } [/code] You just extract all `await.multi` expressions and execute them first, then you execute everything else. So, to address your other concerns: lightmare: > Moreover, `await.multi` cannot span across logical operators, conditional or comma operator: It can span across logical operators, conditionals, and the comma operator. As long as we understand that every `await.multi` expression executes before everything else in the expression, it should be clear what all of these examples do. [code] await.multi f(a) && await.multi f(b); // Both f(a) and f(b) will be called and executed in parallel. We'll `&&` the results. // This doesn't completely defeate short-circuiting // it just means you can't short-cuircut anything inside `await.multi` // because they have to execute first. // But here's another example that short cuircuits the g() call await.multi f(a) && g(await.multi f(b)); await.multi f(t) ? await.multi f(a) : await.multi f(b); // Similarly, f(t), f(a), and f(b) will all execute at the same time, then the results are placed into this ternary operator let a, b; a = await.multi f(b), b = await.multi f(a); // Yes ... this one's a bit unfortunate, but it still makes sense. // `f(b)` executes first and b is undefined. // Then `f(a)` executes and a is undefined. // The results of the two function calls are placed into the expression, // and the rest of the expression is executed. [/code] I can see the concern (especially since I didn't put too much thought into this specific issue until you pointed it out - thanks), but I think that overall, it's not too difficult to understand what's going on. To understand any expression with `await.multi`, first just understand that whatever `await.multi` expressions exist will execute first, then everything else will execute. lightmare: > theScottyJam: > >> Note that nested `await.multi` won't be allowed, so you can't do this: >> >>> `const result = await.multi f(await.multi g())` > > I don't see a problem here, there is clear topological order in which this can be resolved. Well, whether this should be allowed or not can be discussed. I just thought it'll be easier to understand if we make it so all `await.multi` within a single expression execute at the same time, instead of allowing multi-step `await.multi`s to exist. But maybe this isn't all that confusing, and it could be allowed. * first g() executes, because it's in an `await.multi`. The other `await.multi` expression does not execute yet, because it depends on the result of the first. * g() eventually resolves and was the only dependency of the outer `await.multi`, so we can now evaluate the outer `await.multi` expression, calling f() with g()'s result. * Finally `const result = <result of outwer await.multi>` executes. After which, @mmm211 (Matt Morgan) added How about allowing `Promise.all` to accept and return an object [code] async function getData() { const o = await Promise.all({ name: getName(), birthday: getBirthday(), id: getId(), }) return { o.name, o.birthday, metadata: { o.id }, } } [/code] After which, @ljharb (Jordan Harband) added https://github.com/ajvincent/proposal-await-dictionary @lightmare replied theScottyJam: > It can span across logical operators, conditionals, and the comma operator. As long as we understand that every `await.multi` expression executes before everything else in the expression, it should be clear what all of these examples do. I see. It felt counter-intuitive at first; but as a two-pass process it makes sense. So @Clemdz replied This is an interesting topic! :) I've been thinking about it recently, and I'd like to give it a try too. So if I understood correctly, there are actually two main features to be discussed here: * having a non-blocking `await`-like syntax inside async functions * having some syntactic sugar for grouping several promises to be handled at the same time Both those features combined would allow us to create a nice `Promise.all` syntactic sugar, but those could be treated separately (and that's what I'm going for here). ## Non blocking `await`-like syntax I really like the `async const myVar = myPromise()` syntax idea. However, I would rather place the `async` keyword after the `=` sign, like this: [code] const myVar = async myPromise(); [/code] This would have the following pros: * The parallel with the `await` syntax would be greater: * it is acheiving the same purpose than `await` (linearizing promises resolving), but here in a non-blocking way * it would be used the exact same way than `await`, so it is not introducing a completly new concept and syntax * This would also allow to use this `async` syntax without having to declare variables, like: [code] async function () { async updateDB( /*...*/ ); async fetch( /*...*/ ); } [/code] One could argue that in that case, we could simply write instead: [code] async function () { updateDB( /*...*/ ); fetch( /*...*/ ); } [/code] But here is the big difference: using `async` when calling the promises would ensure that those promises have settled before the wrapping async function resolves. This is the important point here, so that the language does not become a spaghetti mess of aynsc-resolving variables. Every promises called using `async` have to settle before the return statement of the function gets executed. Also, compared to the version without `async`, this keyword gives a nice little indication that the function that is called is actually a promise, and not a regular function call. ### Deshugared syntax Taking the simple following code: [code] async function () { const a = async getA(); const b = async getB(); /* ... */ return a + b; } [/code] the desugared version would more or less looks like this: [code] async function () { let a; const _aPromise = getA().then(value => a = value); let b; const _bPromise = getB().then(value => b = value); /* ... */ await _aPromise; await _bPromise; return a + b; } [/code] However, in this desugar version, we have to use `let` instead of `const` for variable declaration. This would a limitation of the desugared version only, in the real implementation, using `const` for async variable declaration should be possible. Also, when awaiting the resolved value, there is the question whether the variable should be set to `undefined`, or left undeclared. If the variable can be declared using const, it makes sense that it stays undeclared, as setting the value to `undefined` and then updating to the resolved value would not be coherent with the `const` declaration. ### Rewriting `Promise.all` At this point, we can already recreate our `Promise.all` example: [code] async function () { let a, b, c; // Await the settlement of every promise inside this async function await (async () => { // Using the async keyword, so that the three promise get executed in parallel a = async getA(); b = async getB(); c = async getC(); // As we are using the async keyword for the promises above // we have to wait for them before leaving the async function })(); // Here, we know for sure that a, b, and c have settled d(a, b, c.property); } [/code] We could also imagine more complex examples: [code] // Same as before, but let's get `a` using the fetch api async function () { let a, b, c; await (async () => { async (async () => { const response = await fetch("url"); a = await response.json(); })(); b = async getB(); c = async getC(); })(); d(a, b, c.property); } [/code] The problem here is that creating wrapper promises isn't really readable. Also, variables created inside the wrapper promises aren't accessible in the parent scope. About this second point, we could instead write: [code] async function () { const { a, b, c } = await (async () => { const a = async (async () => { const response = await fetch("url"); return await response.json(); })(); b = async getB(); c = async getC(); return { a, b, c }; })(); d(a, b, c.property); } [/code] This fixes the issues of variable access, but this is even more difficult to read! ## Grouping promises Here comes the second point of the topic. It would be great to have a dedicated syntax to group `await` and `async` promise calls together. This syntax would need: 1. to be short and readable 2. to have scope coherence and consistency with the language. It could either: * create a new scope, and have a way to return / expose variables to the outside scope * do not create a new scope, and any created variables are directly accessible in the current scope The difficulty about this point is to find a good syntax that meet these two necessary conditions. As you initialy proposed, we could use `{ }` to create `await` and `async` block. (Here I'm changing the meaning you originally gave them, so that their behavior is the same as their "keyword" version) Here is the definition of the `await` block: [code] await { /* ... */ } // instead of await (async () => { /* ... */ })(); [/code] and here is the definition of the `async` block: [code] async { /* ... */ } // instead of async (async () => { /* ... */ })(); [/code] So if we take back our last example, it would become: [code] async function () { let a, b, c; await { async { const response = await fetch("url"); a = await response.json(); }; b = async getB(); c = async getC(); }; d(a, b, c.property); } [/code] However, this solution feels incomplete, because we have to declare variables outside the blocks, using the `let` keyword. ### Other ideas ? We could also go for various other syntax, like the following one which does not create a new scope, thus any created variable is accessible anywhere in the function scope: [code] async function () { await.{ async.{ const response = await fetch("url"); const a = await response.json(); } const b = async getB(); const c = async getC(); } d(a, b, c.property); } [/code] However, the `.{ }` syntax is not used anywhere else in the language, and this looks quite weird. I'm still presenting this idea, in case of it can inspire someone for something better, but I would personally not go for this one :) Here is a last idea: [code] async function () { await ( async ( const response = await fetch("url"); const a = await response.json(); ); const b = async getB(); const c = async getC(); ); d(a, b, c.property); } [/code] Here, using parentheses makes it clear that we are not creating a new scope, and that the variables are accessible anywhere in the function. However, it looks quite strange too, as it is not something we can already do in the language. Like the previous idea, I do not quite like this solution, I'm just sharing it in case it appeals to someone! ## To conclude The `async` keyword used as a non-blocking `await` keyword could be quite versatile and powerful, but it could also mess up the language by making it even more complex to understand. What do you think about it? Also, such an `async` keyword would be quite limited if it does not come along with a way to group certain promises together, because creating wrapper async functions is quite heavy and difficult to read. However, finding a great syntatic sugar for this is complicated. So if someone has some ideas, please share them! :) (and more generally, please feel free to share your thoughts about all of this!) @theScottyJam (Scotty Jamison) replied @Clemdz \- interesting idea. Finding an intuitive syntax for promise.all() is much harder than it sounds :p So, in your proposal, when you use async on a promise, at what location does the await actually happen? Right before the target variable gets used? What if you do something like "const x = async f() + async g()", when do these get awaited? So @Clemdz replied That an interesting question you are asking there! :) But before trying to answer to it, I'd just like to propose a new idea for the `await` / `async` block syntactic sugar, inspired right from this very recent topic about returning values from a block! :) (I'm doing this right now, so that I'll be able to use this syntax later in my explanations) ## New syntax proposal for `await` / `async` blocks As discussed in the aforementioned topic, the proposed `do { }` block is returning the last expression value: [code] const foo = do { const x = f(); const y = g(); x + y } // foo will be equal to x + y [/code] So actually, why couldn't we apply the same concept to `await` and `async` blocks? [code] const foo = await { const x = async f(); const y = async g(); // wait before returning that x and y settle x + y; } // foo will be equal to: x + y const bar = async { const x = await f(); await g(x); } // bar will be equal to: await g(x) [/code] This would be as simple as that! So let's try answering your question about combining asyncs in a same expression! ## A solution to your question Let's take the example you gave in your question: [code] const x = async f() + async g(); [/code] Here, we are not only _setting_ an async value, but we are also trying to _get_ the value, in order to do some operations with it. However, as the first `async f()` has not settled yet at the time of the operation, we actually should get a `ReferenceError`, in the same spirit like when we try to use an undeclared variable. Rewriting your example like this: [code] const tmp1 = async f(); const tmp2 = async g(); const x = tmp1 + tmp2; [/code] it become clear that we are trying to use async variables that may not have resolved. So instead, you would have to write: [code] const x = async { const tmp1 = async f(); const tmp2 = async g(); // wait for tmp1 and tmp2 to settle before returning from the scope tmp1 + tmp2; } [/code] or actually, even simpler: [code] const x = async { // wait for both asyncs settle before returning from the scope async f() + async g(); } [/code] So this would be almost the same code as your given example, but with an wrapper `async` block, indicating where the f() and g() have to be resolved so that we can continue without any problem, and assign `x` to the sum of their value. ## Personnal thoughts about this This is the most minimal and coherent answer I can give you using the premises I defined in my own proposal. However, I have to admit I don't really like in the first place the concept of: "you can't access the value of an async variables, except when it is the last returned instruction of a scope, there you can use their value as if their were normal variables". It makes sense why this is a thing within the whole idea I'm trying to propose, but I'm afraid this might actually not be very intuitive when writing or reading code. People might just be confused why is this possible to write `async f() + async g()` in some places, and not in some others. The new block syntax I'm proposing makes it even more confusing, because no `return` keyword is even here to explicitly indicate we are actually in a return expression. Actually, waiting for async variables to settle would make way more sense if we are waiting right before leaving the scope of the function, and not just before we execute the last expression of it. Like this, every expression of the scope would be treated as equal: you can't access an async variable value because it could still be undeclared (and it would throw a _ReferenceError_ ). No matter if it is a return expression or not. So with this new approach in mind, this would look like: [code] let tmp1; let tmp2; async { tmp1 = async f(); tmp2 = async g(); // Return nothing here // Wait just before the block that tmp1 and tmp2 have settled } const x = tmp1 + tmp2; [/code] The only con of this approach, and that's why I didn't proposed it in the first place, is that it does not allow to return the value of the inner async variables to the outer scope. At least not without implementing some new syntax just for this. So we have to declare our variables directly in the outer scope, using `let` instead of `const`. Yikes. So in the end I am with two possible approaches: 1. awaiting before the return expression: it it easy to use, but might be too confusing 2. awaiting just before leaving the scope: it is not confusing at all, but is much less easy to use This leads me to the following two questions: 1. What do you think about the 1st approach? Do you think it is too much confusing, or not confusing at all, or maybe something in the middle? 2. What are your thoughts about this 2nd approach, and do you see any way to improve it so that it become usable? So actually I'm sorry @theScottyJam, in the end I'm answering your question by raising two of them! Indeed, finding an intuitive syntax for `promise.all()` is much harder than it sounds! :) To which @theScottyJam (Scotty Jamison) replied Ah, I'm understanding things better now. In your second approach, what would this do? [code] let tmp1 = 1; let tmp2 = 2; async { tmp1 = async f(); tmp2 = async g(); console.log(tmp1 + tmp2) } [/code] Does this log 3, because it used the old value of tmp1 and tmp2? Or is it a reference error? Does assigning to a variable put the variable in an undefined state until the promise has settled? * * * I'm going to focus on your first approach, as that currently makes more sense to me, due to the issue above. In this approach, if we ignore the case where you put `async` in the last expression (I'll come back to that later), then there's really no difference between this in the `parallel const` idea, except for potentially nicer syntax, and the fact that there's better support for fire-and-forget. For example: [code] const res = async { const x = async f() const someParam = calcParam() const y = async g(someParam) async fireAndForget() x + y } // is the same as const someParam = calcParam() parallel const x = async f(), y = async g(someParam), _ = fireAndForget() const res = x + y [/code] Even though the `async {}` syntax allows for any arbitrary synchronous expression to be intermixed between the async ones, there's actually no need to do so. Those synchronous expressions can't ever depend on the results of these async operations, so they can always be placed beforehand instead of being intermixed. The `async` construct itself can only ever be used to assign to something, or in a fire-and-forget scenario - it's invalid to try and use it in any other way, as that would require using the resolved value, which would be a ReferenceError within the block (if we're not at the last expression). I'm mostly pointing this out, because this `async {}` syntax has the appearance of being able to have much more flexibility than `paralell const`, but in reality, I think they're almost the same, except for the extra power it has in the last expression. This isn't a bad thing - it just took me some time to figure out that there wasn't anything extra going on with this except for a different syntax. Now, about `async` being used in the last expression (e.g. `async { async f() + async g() }`) - from what I can understand, this will have pretty much the same semantics as the await.multi idea. You're going to run into the same issues that async.multi had, where we need to define a deterministic execution order for this expression. I'll just assume that it uses the same algorithm as await.multi (where it executes everything after `async` first, then the rest of the expression), unless you have a different idea you want to share. * * * Alright, with all of that in mind, I'll try to build yet another proposal using these previous ideas :p. I see that my previous `await.multi` syntax is not as powerful as the `async {}` block idea, but I also feel like there's some clumbsy parts to `async {}` that I want to take a sarah09@example.net. I'm proposing a `do interpolated` block, which acts like a do block, but you're allowed to interpolate expressions into it that run before the do block runs. Example: [code] const result = do interpolated { await ${fireAndForget()} const value = await ${f()} value + await ${g()} + await ${h()} } // is the same as const result = do { const $temp1 = fireAndForget() const $temp2 = f() const $temp3 = g() const $temp4 = h() await $temp1 const value = await $temp2 value + await $temp3 + await $temp4 } [/code] This will let all of the async operations run in advance, before the actual do block executes and we start running things one-at-a-time. I think the interpolation syntax helps make it clear that the particular expression can't depend on anything created within the `do block`, i.e. this would be an error: [code] do interpolated { const x = f() 2 + ${g(x)} // Error! x is not defined. } [/code] Thoughts? Then @claudiameadows (Claudia Meadows) said I have a better (and simpler) idea, one that probably addresses this and several other proposals way better, blending async and sync without sacrificing explicitness or performance (and in reality improving performance somewhat even for optimal code). Here's a short demo: [code] // Fetch thing A await let resultA = fetch("thing A") await let a = resultA.json() // Fetch thing B - done in parallel with `resultA` because it doesn't depend on it! await let resultB = fetch("thing B") await let b = resultB.json() // If an error occurs, it stops here, not above. console.log([a, b]) [/code] The idea is that you specify the variable as async, and it gets implicitly awaited before first use. The basic production is `await var a = b` (or similar with `let` or `const`), and everything else extends from there. It's simple to learn, and extremely easy to use. Then @theScottyJam (Scotty Jamison) said Nice idea! I think I like that better than my parallel const idea. It, unfortunately, doesn't support the use case of doing multiple awaits in a single expression very well. e.g. [code] const data = { x: await.multi getX(), y: await.multi getY(), } [/code] Maybe my hope to support that situation in the same proposal is wishful thinking though and can be considered as a separate issue if there isn't an intuitive way to do so. I do have one question with your `await const` proposal - what happens in this scenario? [code] async function doSomething(data) { await const value = fetch('something') return data.map(item => item + value) // This lambda is not async } [/code] (Also, your link is broken - could you fix that?) After which, @claudiameadows (Claudia Meadows) added theScottyJam: > (Also, your link is broken - could you fix that?) Done. Forgot to make it public. theScottyJam: > It, unfortunately, doesn't support the use case of doing multiple awaits in a single expression very well. e.g. [code] > const data = { > x: await.multi getX(), > y: await.multi getY(), > } > [/code] That's by design. I want to keep it simple, and that...isn't simple. That can be a follow-on proposal (if you can find a semantic that doesn't lead to the pitfalls I explain in the proposal regarding child function contexts), but I explicitly want to keep it as simple and narrow as possible. **Edit:** You could simulate that with this: [code] await const dataX = getX() await const dataY = getY() const data = { x: dataX, y: dataY } [/code] There's intentionally no first-class support for it per my explanation regarding child function contexts, though. (That's only part of it - there's of course many other nasty demons I chose to avoid in that.) theScottyJam: > I do have one question with your `await const` proposal - what happens in this scenario? [code] > async function doSomething(data) { > await const value = fetch('something') > return data.map(item => item + value) // This lambda is not async > } > [/code] Search for "child function contexts" in the proposal - that should answer it pretty well. (See my first part of this reply - you likely only asked because I forgot to make the repo public. ) So @theScottyJam (Scotty Jamison) replied Ah, you've got a pretty thorough explanation in there about everything :) So, you said the following in there: > Awaited variables are implicitly awaited, with errors thrown as applicable, before being placed in child function contexts, regardless of type. Lets take the following example: [code] async function doSomething() { const async x = fetchX() const async getX = () => x // <---- const async y = fetchY() return x + y } [/code] Does this mean the `x` would get awaited before the `<----` line gets run? And if you wanted fetchX() and fetchY() to run in parallel, you would have to move the `getX()` definition further down the function? It's a little unfortunate, but it probably makes the most sense. To which @claudiameadows (Claudia Meadows) replied That is the correct understanding, yes. And I certainly tried to come up with a solution, as you could tell in that explainer. Then @lightmare said Appreciate you wrote that up. I really like you chose `await var`, it much better conveys declaring a variable that'll be awaited, than my previous `async var` suggestion. Also great you mention cancellation. That's something I definitely wanted to happen to variables that go out of scope before use, but was too lazy to write that up, given that cancellation is not a thing in JS yet. Now to my issues with your proposal ;) While implementing `async var` proof of concept, I realized one has to explicitly forbid mixing `var` with `await var`. Mixing `let` with `await let` is not an issue, because declaring a lexical twice is already illegal. But `await var` declarations are tricky, in that they could change the meaning of plain `var` somewhere else, which is one of the reasons I eventually dropped the ball on `async var`. `await let` is also tricky, because then you have to specify whether it affects assignment as well. [code] await let x = JSON.parse("garb-age"); // rejection x = JSON.parse("wut?"); // throw immediately, or wrap in rejection? [/code] claudiameadows: > Awaited variables not used by the end of the function simply continue. If they result in rejections, those will simply translate to unhandled rejections as applicable (this will become handy once `async do` expressions come along). This ain't good. It would cause the exact same problem `Promise.all` currently has. And with `await let` it has the same problem as reassigning a variable without awaiting the promise it held previously. [code] await const a = Promise.reject(); await const b = Promise.resolve(1); await const c = Promise.resolve(2); return c ? b : a; [/code] I have a proof of concept in the works for a `future` keyword (akin to your `await const`). After some consideration, I came to the conclusion that variables that go out of scope before use should not only be cancelled (once cancellation becomes a thing), but their rejections silently ignored. If you never needed the value, you don't care about failures in getting that value. claudiameadows: > Awaited variables are implicitly awaited, with errors thrown as applicable, before being placed in child function contexts, regardless of type. This ensures the looseness in timing is purely restricted to the function they're declared in. When is "before being placed"? Consider this example: [code] async function foo() { await const a = new Promise(res => setTimeout(res, 100, 14)); await const b = new Promise(res => setTimeout(res, 100, 28)); return f; function f() { return a + b; } } [/code] claudiameadows: > * I'm also open to banning such references outright, but that would also risk confusion as users would be wondering why they can't just use what looks like an ordinary variable reference. There's already enough magic, and I don't want to add even more apparent magic than what already exists. > Getting confused by a syntax error is way better than getting confused by obscure runtime behaviour. Such as a variable being awaited (and possibly throwing an error from rejection) at the point of declaration of a function that might be using it, i.e. before the function is called, and without the function ever actually reading that value: [code] // why should this code block for 200ms? await const a = new Promise(res => setTimeout(res, 200, 42)); const f = x => x ? x * a : 0; return f(0); [/code] In other words, in my proposal all references to the variable are simply replaced with an await, meaning the above code transpiles to invalid code with an easily explainable error: [code] // transpiled const a = new Promise(res => setTimeout(res, 200, 42)); const f = x => x ? x * (await a) : 0; return f(0); [/code] claudiameadows: > * Allowing async references to propagate in non-resolved state to child `async` functions and similar is a _bad_ idea: it's not clear why `Promise.all(users.map(x => doSomething(x, asyncVar)))` shouldn't be functionally equivalent to `Promise.all(users.map(async x => doSomething(x, asyncVar)))` just because `asyncVar` was defined differently, and I don't expect anyone would intuitively think otherwise absent deep knowledge of awaited variables' semantics. > To me it's not clear why you would want `Promise.all(users.map(x => doSomething(x, asyncVar)))` to be legal syntax. You're referencing an awaited variable in non-async function. "just because `asyncVar` was defined differently" is a pretty good reason in my book. The whole point of defining it differently is to make it behave differently. After which, @claudiameadows (Claudia Meadows) added Feel free to file those as issues against my repo if you haven't already, and I can address them there. It'd be much easier to address piecemeal over there. @rdking (Ranando D Washington-King) replied I know it's been almost a year, but I noticed something with all of the logic presented. Things that need to be collectively awaited are almost always grouped together. That's why the notion of an await block sound good for this, but doesn't it all lend itself to a much simpler solution? Automatically awaiting assignment to variables can be a messy and confusing thing if functions get a little long. So I'm not a big fan of the `await const` style declarations. Instead, I'll take a cue from @Clemdz and offer this: What if, like started suggesting, we use `async` as a non-blocking `await`? Well, that means code will continue to fall through, possibly hitting something requiring one of the `async`'d calls to be resolved. Since the original goal is to simplify the syntax for `Promise.all`, why not just continue to use `await` as a measure of flow control? [code] async () => { let a = async getA(); let b = async getB(); let c = await getC(); return await doSomething(a, b, c.prop); } [/code] This would be the same as: [code] async () => { let [a, b, c] = await (new Promise.all([ getA(), getB(), getC() ])); return await doSomething(a, b, c.prop); } [/code] Since `await` makes you wait for the result, it can be used as the cue to wait for all async calls made prior to it in the same function. Benefits: * No unnecessary structuring in the function. * Only 1 new syntax. * Flow control remains clear, concise, and explicit. * Errors can still be quickly discovered as unresolved assignments from `async`'d Promises will be in a TDZ. IMO, this should be far simpler to implement and comprehend than these notions of `async`/`await` blocks. After which, @aclaymore (Ashley Claymore) added It's an interesting idea, though it might surprise people that rejections from `getA()` would appear to come from `await getC()` To which @rdking (Ranando D Washington-King) replied I don't see this as all that big of an issue. [code] async () => { let, a, b, c; try { a = async getA(); b = async getB(); c = await getC(); } catch(e) { console.error(e); } return await doSomething(a, b, c.prop); } [/code] Since the `Promise` instances are hidden, is there any real point in trying to determine where the error came from? If there is such a need, why not just wrap each individual call in an exception handler? I can imagine a way of handling the exceptions on a per `Promise` basis, but I don't think it's worth the effort to toy around with exception handling the way this would require. Better to just attach a `.catch` handler to the call. [code] async () => { let, a, b, c; a = (async getA()).catch(console.error); b = (async getB()).catch(console.error); c = (await getC()).catch(console.error); return await doSomething(a, b, c.prop); } [/code] The expansion of this would be the same as: [code] async () => { let handlePromise = (fn) => { return new Promise((resolve, reject) => { try { resolve(fn()); } catch(e) { reject(e); } }).catch(console.error); } let [a, b, c] = await (new Promise.all([ handlePromise(getA), handlePromise(getB), handlePromise(getC) ])); return await doSomething(a, b, c.prop); } [/code] The point is that the `Promise.all` generated by the `await` following one or more `async` calls should not necessarily appear to be the source of all errors. In the end, the `async`/`await` syntax is meant to hide the `Promise` objects and make the syntax appear to be more sequential and easy to understand, right? Then @theScottyJam (Scotty Jamison) said This is an interesting idea. Let me throw another wrench in it though. How would this behave? [code] obj.x = async task1(); // may or may not trigger a setter function console.log(obj); console.log(async task2()); const x = transform(async task3()); const { a: { b } } = async task4(); // Assume this promise resolves to `undefined` await something(); [/code] You've pared this `async` operator with a declaration in your examples, and then are treating the declaration as if it's in a TDZ until after the next `await`, but what if the user isn't immediately assigning the output to a simple variable? What if they're doing something else with it first? So @rdking (Ranando D Washington-King) replied The first line throws the value of `obj.x` into question. If `obj.x` already existed, then there's no issue and the next line reports the old value.
{'source': 'discourse', 'url': 'https://es.discourse.group/t/concurrent-async-and-normal-await-evaluation-blocks/827', 'title': 'Concurrent Async and Normal Await Evaluation Blocks', 'views': 665, 'has_accepted_answer': False, 'word_count': 8681, 'like_count': 10, 'reply_count': 19, 'participant_count': 10, 'posts': 35, 'tags': 'proposal', 'author': '@wlib (Daniel Ethridge)', 'participants': '@wlib (Daniel Ethridge), @theScottyJam (Scotty Jamison), @aclaymore (Ashley Claymore), @Jonas_Wilms (Jonas Wilms), @lightmare, @mmm211 (Matt Morgan), @ljharb (Jordan Harband), @Clemdz, @claudiameadows (Claudia Meadows), @rdking (Ranando D Washington-King)', 'image_url': [None], 'created_at': '2021-06-25T01:24:11.738Z', 'last_posted_at': '2022-05-24T04:11:00.653Z'}