asyncawait - a function declaration keyword to have callers automatically await result

await/async could be a powerful way to get coroutine like functionality from Javascript but while adding async to function/method declarations isn't much of a burden, adding await on every function call makes code so difficult to read that it's effectively unusable to apply throughout a code base without a transpiler. Perhaps this problem could be resolved by adding a new keyword "asyncawait" (or whatever name people would prefer) with the semantics of when it's called, it's treated as if the caller had used an await on the call.

If there's a concern that this limits the ability of the caller to choose when it wants a promise on an asyncawait declared function, then a 2nd keyword on the caller side to sidestep the automatic await and return the promise could be used.

I don't know if this is the best way to solve this problem and would welcome other suggestions that achieved the same goal. Thanks for listening.

When you await a call to an asynchronous operation, you are introducing a concurrency interleave point. One of the characteristics of JavaScript's innate Event Loop Concurrency model is that it is much more robust against a wide variety of common concurrent programming errors, in large part because the interleave points are both less common and explicitly marked. A feature like what you describe would make the interleaving invisible at the call site, which is extremely perilous. If your code has a large number of awaits, that is potentially diagnostic of other structural concurrency problems that are probably better attacked directly.

5 Likes

I totally disagree: having await in the caller makes the code explicit about things being async, and thus more readable and semantic in telling downstream developers what is happening.

In other languages, where you don't know if a given call is async (or multithreaded, f.e. Java), this can lead to misunderstanding of the operation of the program. The lack of explicitness makes it easier to create race conditions and other issues.

Explicitness tells downstream code what it happening, so downstream developers can have solid expectations. The presence of await helps people better understand code execution order.

2 Likes

trusktr writes:

"I await totally await disagree: await having await await await in await the await caller await makes await the await code await explicit await about await things await being await async, await and await thus await more await readable await and await semantic await in await telling await downstream await developers await what await is await happening. await In await other await languages, await where await you await don't await know await if await a await given await call await is await async await (or await multithreaded, await f.e. await Java), await this await can await lead await to await misunderstanding await of await the await operation await of await the await program. await The await lack await of await explicitness await makes await it await easier await to await create await race await conditions await and await other await issues. await Explicitness await tells await downstream await code await what await it await happening, await so await downstream await developers await can await have await solid await expectations. await The await presence await of await await await helps await people await better await understand await code await execution await order."

You're right. The explicit version is much easier to read.

What if the caller isn't async?

An alternative worth considering is a form of async function that awaits all potentially promise-returning expressions by default if they're thenable, and then just allowing an escape hatch for sync calls that need to resume immediately and async calls that need to be always awaited.

IMO it's a bad idea to seek for extreme code simplification. Oftenly, simplifying will paradoxically leads to complexity (It takes more time for programmer to process the code and understand it).

For example :

ESLINT default configuration disallow post incrementation

i += 1; is easier to understand than i++;

Also, I find it better to specify the keyword this when calling an inner method inside of a class.

const outside = () => console.log('outside');

class Foo {
    private gold() {}

    public bar() {
       this.gold();

       outside();
   }
}

When the right keywords are used, to can instantly know what is happening.

Same about await, it tells you that you have to deal with an asynchronous call. An API call ? A database request ? A file reading ?

Simplifying is good, but be careful to not remove in the process valuable information.

What is easier ?

const a = [1, 2, 3, 4, 5, 6];

// Loop over all values of the array
const res = a.reduce((tmp, x, xi) => {
  // Use Math.floor and xi (the index of the value we are treating)
  // to store the values on the returned array at the correct position
  tmp[Math.floor(xi / 2)] = (tmp[Math.floor(xi / 2)] || 0) + x;
  
  return tmp;
}, []);

console.log(res);

Or

let array = [1,2,3,4,5,6];
let result = array.reduce((a, v, i) => ((i % 2 === 1 ? a.push(v + array[i - 1]) : 0), a), []);
console.log(result);

?

I think the await keyword is crucial because it makes it clear that state could have mutated in-between.

For example:

let state = 1;

setTimeout(() => {
  state = 2;
}, 1000);

console.log(state); // state === 1
await doSomethingWhichTakesAFewSeconds();
console.log(state); // state === 2

Without the await keyword, this would be extremely confusing especially since doSomethingWhichTakesAFewSeconds() doesn't itself mutate the state variable.

On the other hands, when calling functions that are not awaited, you get that nice guarantee that top level state cannot mutate behind the scenes without your knowledge.

This is not a problem with pure functions but forcing users to only use pure functions has its own drawbacks in terms of allowing good, well-encapsulated class-level abstractions.

If you're concerned about unexpected state mutation, doesn't async/await makes this possibility far more likely as every await function return is forced to act as a yield, while with coroutines yields are only done where needed (which is typically only when waiting on an async i/o call)?

IMO a key aspect of await is that it allows other parts of the code to run in parallel. With this in mind, I don't see how state mutations can prevented; I think that's a tradeoff that cannot be avoided.

For example, a lot of developers would find this counter-intuitive:

(async () => {
  if (state === 1) {
    doSomethingWhichTakesSomeTime();
  }
  if (state === 1) {
    doSomethingElseWhichTakesTime();
  }
})();

A lot of developers will want to refactor to this:

(async () => {
  if (state === 1) {
    doSomethingWhichTakesSomeTime();
    doSomethingElseWhichTakesTime();
  }
})();

But that would be incorrect because according to this idea of await by default, the state could have changed in between.

That said it could just be a matter of changing the developer mindset. I just want to point out the possible confusion.

1 Like

I wonder if it could be possible to invert async/await to await/async for example:

(await () => {
  let result = doSomethingWhichTakesTime();
  async doSomethingElseWhichTakesTimeButDontWait(result);
  waitSomeMore(100);
  
  // This can be used as a for-await-of loop:
  async for (let hello of asyncIterableStream) {
    // This doSomethingTimeConsuming() function only blocks the scope
    // of the for loop but doesn't block what comes after the for loop.
    doSomethingTimeConsuming();
  }

  // The for loop above runs in parallel to this code because it was marked
  // with async.
  foo();
})();

Actually this would be an interesting way to solve the problem that I'm having: