Concurrent Async and Normal Await Evaluation Blocks

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

2 Likes