Concurrent Async and Normal Await Evaluation Blocks

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

a b c
| | |
| | (c => c.property)
\ | /
  d

We can implement it intuitively like this:

const a = getA()
const b = getB()
const c = getC()

d(a, b, c.property)

But what about when a, b, and c are promises?
It could be translated directly from above like this:

const a = await getA()
const b = await getB()
const c = await getC()

d(a, b, c.property)

But if we optimally implement this for the given dependency graph, we get this:

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)

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:

async {
  const a = getA()
  const b = getB()
  const c = getC()
}

d(a, b, c.property)

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:

async {
  const resultA = concurrentProcessA()
  concurrentProcessB()
  await {
    await concurrentProcessCStep1()
    await concurrentProcessCStep2()
    await concurrentProcessCStep3()
    console.log("The normal evaluation order we are used to")
  }
  concurrentProcessD()
}

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

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()

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.

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

const [a, b, c] = await.all [getA(), getB(), getC()];

So, with your current proposal, how do you propose something like this works?

async {
  const myArray = []
  myArray.push(asyncFn1())
  myArray.push(asyncFn2())
}

Is myArray allowed to have [<result of asyncFn2()>, <result of asyncFn1()>] when this is done executing? Why or why not?

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

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.

const [var1, var2, var3] = await Promise.all([
  getA(),
  getB().then(x => x.y),
  getC(),
  doSomethingElse()
])

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.

parallel const
  var1 = await getA(),
  var2 = await getB().then(x => x.y),
  var3 = await getC(),
  _ = doSomethingElse()

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?

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:

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

transpiles to:

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

Because I think async const is too long yet the most useful, I made future a contextual keyword equivalent to async const.

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.

future x = someAsyncTask()
// ...
const f = () => x
console.log(f())

I assume that these async variables can only be used inside of async functions, so that will just result in an early error.

It's mere syntax sugar. Instead of typing await where you need the result, you type async where you're starting the computation.

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.

Sounds great, that would mean it does more useful stuff than swapping one keyword for another.

Yes. That's not new: { console.log(x); let x; }

That last point is correct: you basically have await x in sync function.

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.

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

I'm not saying this is necessarily a bad thing, but it is different.

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.

// 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 },
  }
}

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.multis 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:

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()

Note that nested await.multi won't be allowed, so you can't do this:

const result = await.multi f(await.multi g())

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.multis get grouped together into a single Promise.all() effect)

I don't think await.multi is viable. Allowing it on arbitrary expressions would wreak havoc in evaluation order.

r = [ x(await.multi f()), y(), z(await.multi g()) ];

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:

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

I don't see a problem here, there is clear topological order in which this can be resolved.

This one is the answer:

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:

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

You just extract all await.multi expressions and execute them first, then you execute everything else.

So, to address your other concerns:

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.

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.

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.

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

How about allowing Promise.all to accept and return an object

async function getData() {
  const o = await Promise.all({
    name: getName(),
    birthday: getBirthday(),
    id: getId(),
  })

  return {
    o.name,
    o.birthday,
    metadata: { o.id },
  }
}
1 Like

https://github.com/ajvincent/proposal-await-dictionary

2 Likes

I see. It felt counter-intuitive at first; but as a two-pass process it makes sense.

1 Like

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:

const myVar = async myPromise();

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:

async function () {
  async updateDB( /*...*/ );
  async fetch( /*...*/ );
}

One could argue that in that case, we could simply write instead:

async function () {
  updateDB( /*...*/ );
  fetch( /*...*/ );
}

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:

async function () {
  const a = async getA();
  const b = async getB();
  /* ... */
  return a + b;
}

the desugared version would more or less looks like this:

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

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:

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

We could also imagine more complex examples:

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

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:

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

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:

await {
  /* ... */
}
// instead of
await (async () => {
  /* ... */
})();

and here is the definition of the async block:

async {
  /* ... */
}
// instead of
async (async () => {
  /* ... */
})();

So if we take back our last example, it would become:

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

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:

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

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:

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

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

1 Like

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

1 Like

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