JavaScript native fibers

[Introduction]

When we need to wait for the result of an asynchronous function, we can use promises or better async/await :

console.log("wait for result...");

const result = await asyncQuery();

console.log("result =", result);

Now, consider the following scenario: we wrote a complex code with many function calls that were synchronous:

const a = () => { ... return 1 + b(); };
const b = () => { ... return 2 + c(); };
const c = () => { ... return 3 + value; }

Suppose that for some reason c now needs to call an asynchronous API.

  • So we need to turn c into an async function: async () => { ... return 3 + await api(); }.
  • We also need to transform calls to c with await: ... return 2 + await c().
  • Since functions that depend on c must use await, they in turn become asynchronous.
  • ...

These cascading transformations can be very complicated to manage, especially if the project is large. Of course, the best thing to do is to foresee the functions that could become asynchronous beforehand to avoid this work, but this is not always obvious.

[Proposal]

It would be great if we could write something like:

console.log("wait for result...");

const result = wait asyncQuery();

console.log("result =", result);

Thanks to this syntax, we can rewrite c very simply by introducing wait without having to rewrite the other functions:

const c = () => { ... return 2 + wait api(); };

We can get close to this solution with fibers but the project is not maintained anymore.

It would be great if JavaScript could introduce this concept natively and be used on both client and server side.
What do you think about it?

Is the problem of having to convert a chain of functions to async await really all that difficult and widespread to warrant introducing a new syntax that adds more complexity to the language and gives developers something else to learn?

Not really. Putting async on the functions and await on all the usages is trivial, it could be automated by the IDE if you wanted.
The hard part is thinking about what it means for those functions to become asynchronous, how they would be allowed to run concurrently, how they might deal with reentrancy now, etc. Keeping the code correct, introducing locks where it is now necessary to protect state shared between multiple executions, is where this becomes complicated.
A new syntax where it's not explicit where async code is interleaved will not help with that, I'd argue it actually makes the situation worse.

3 Likes

And, remember, simply putting await in front of the function call isn't always the best option.

Take this example:

async function f() {
  await g()
  h()
}

Now you want to make h() asynchronous. If h() doesn't have to run after g(), then perhaps the better solution would be to do this:

async function f() {
  await Promise.all([
    f(),
    g(),
  ])
}

If there was a bit of logic between f() and g(), you may have to move some logic around to bring those function calls closer together, so you can put them into the same Promise.all().

And, as @bergus mentioned, there's also the fact that you don't have the same guarantees about the state of your data when dealing with synchronous logic vs asynchronous logic.

In general, it takes more work and thinking to make code good, asynchronous code. It's not something that can simply be automated. The fact that you have to, one by one, go through each function and make it async is a good thing, because it forces you to look at each function definition as you do so, and make sure the function will still work ok once it becomes async, and it gives you a chance to look for potential ways to improve the logic within the function to better handle its new asynchronous nature.

1 Like

Thank you for your answers!