Concurrent Async and Normal Await Evaluation Blocks

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:

const foo = do {
  const x = f();
  const y = g();
  x + y
}
// foo will be equal to x + y

So actually, why couldn't we apply the same concept to await and async blocks?

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)

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:

const x = async f() + async g();

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:

const tmp1 = async f();
const tmp2 = async g();
const x = tmp1 + tmp2;

it become clear that we are trying to use async variables that may not have resolved.

So instead, you would have to write:

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;
}

or actually, even simpler:

const x = async {
  // wait for both asyncs settle before returning from the scope
  async f() + async g();
}

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:

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;

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! :)