DEV Community

Anurag Vohra
Anurag Vohra

Posted on • Updated on

Proposed a new syntax await[timeoutInMs] to TC39 forum: Please review and comment

Detailed problem description and solution

We all are aware with usage of await of a promise: it basically commands code to wait for a promise to resolve or reject..... but wait until upto when? Indefinitely actually!
As of now any asynchronous promise based code's eventual destiny is at mercy of the asynchronous source.

The asyncronous source has full power to keep all resource on stack of an async process engaged on RAM, and developer seems to have no control on it, as async source can decide when should it resolve (or never resolve) a promise, there by engaging everything on RAM.

Consider this code:

let someReallyBigItemOnRAM = getSomeBulkyValue();
let res = await someReallyTimeConsumingAsyncFunction(someReallyBigItemOnRAM);
Enter fullscreen mode Exit fullscreen mode

Now in this someReallyTimeConsumingAsyncFunction can take really long time to return or say never return and keep someReallyBigItemOnRAM on RAM engaged on RAM forever!

To overcome this problem, a JS developer must have contol over await. A new code will look something like this:

let someReallyBigItemOnRAM = getSomeBulkyValue();
try{
let res = await[1500] someReallyTimeConsumingAsyncFunction(someReallyBigItemOnRAM);
}catch(e){
  //try catch is used as await[timeInMs] can cause a timeoutError, which needs to be caught
  console.error(e);
}
Enter fullscreen mode Exit fullscreen mode

Such an await will await for at most 1500 ms, else it will generate an timeout error.
NOTE: It is assured that if used without timeout the await will exactly behave as it has always has behaved, so no old code is ever going to fail due to this new enhancement. User will still be able to use await without timeout.

Now one proposition that comes to mind is usage of Promise.race to simulate what is intended here:

let timeout = (time)=>new Promise(res=>setTimeout(res,time));
let someReallyBigItemOnRAM = getSomeBulkyValue();
let res = Promise.race([timeout(1500),someReallyTimeConsumingAsyncFunction(someReallyBigItemOnRAM)]);
Enter fullscreen mode Exit fullscreen mode

But Promise.race has a flaw which do not suffice the requirement.
It will though ignore the value returned by someReallyTimeConsumingAsyncFunction function, if its not finished before timeout, but it do not interrupts its execution. Indeed your code will never exit and neither the someReallyBigItemOnRAM will be released until the promise of someReallyTimeConsumingAsyncFunction is resolved. Your virtually have no control on someReallyBigItemOnRAM now. It is at mercy of async source when they want to release it!

Async for await loops

Consider this code:

for await(let s of anAsyncGeneratorOrStream){
//do some thing here
}
//once loop finish do shomething after wards
Enter fullscreen mode Exit fullscreen mode

Again the anAsyncGeneratorOrStream has full power to keep this loop running for ever with developer having no control. As the source is asynchronous, it can send data at interva of its own will, and can take forever to complete if it wants.
However if we have an await[timeInMs] syntax available as well with regular await:

try{
  for await[3000](let s of anAsyncGeneratorOrStream){
  //do some thing here
  }
}catch(e){
//catch time out error if above code throws it
}
//once loop finish do shomething after wards
Enter fullscreen mode Exit fullscreen mode

We can be assured that we will get out of such a loop by at most 3000 milliseconds.
A much better control in hands of a developer.

Again there are codes to simulate this kind of timeout loops using Promise.race, but as like before Promise.race will ignore the value returned by LongRunning async code but will not stop it from holding RAM and values on stack until async promise it had is finished, even though we intended to ignore such timed out values.

Why is it required/matter ?

  1. Better control on developer end, rather than at mercy of asynchronouse function.
  2. Can give much more better understanding of how long can a particular line can at most take and can help pin point bottleneck in the code.
  3. Is very simple to implement, as the code simply generate Timeout error. try/catch and async/await are part of JS. An await[timeInMs] is possible source of a timeout Error, and hence compiler can pre warn user about potential timeout points in the code.

What are the fears, and they indeed are not to worry

Argument: A code can't be made to break/interrupted in between, it can cause potential resource leaks. That is some resource which were supposed to clean up but were interrupted by timeout error, will be in leak stage.
Consider this problem (Problem 1):

async function doLongRunningTask() {
  const connection = await getConnectionFromPool()
  const { error, resource } = await connection.fetchResource()
  connection.release()

  if (error) throw error
  return resource
} 
Enter fullscreen mode Exit fullscreen mode

If such a code is interrupted before a call to connection.release() is made, then it will evetually cause leak.

await[3000] doLongRunningTask();//its feared that this new syntax can cause leaks inside long running task, as if it takes too long it will raise an error and will not get time to call connection.release()
Enter fullscreen mode Exit fullscreen mode

But it should be noted that developer has deliberatly written await[timeInMs] , and user knows that it will cause an error to be raised.
When something is deliberate, all repercussions, are not unexpected, they are intended outcomes.

User can create such deliberate problem, by writing a code as such to the same problem without using await[timeInMs]:
(example 1)

//deliberate attempt to mess up some one's api code:
let t = getConnectionFromPool;
getConnectionFromPool = ()=>setTimeout(a=>throw "error",100); return t();

async function doLongRunningTask() {
  const connection = await getConnectionFromPool()
  const { error, resource } = await connection.fetchResource()
  connection.release()

  if (error) throw error
  return resource
} 
Enter fullscreen mode Exit fullscreen mode

Both will have same effect and , are done deliberately, and hence user knows what is about to come.
An api that intend to do a must have clean up , would have rather written code as such.

async function doLongRunningTask() {
let connection;  
try{
 //now any where any exception occurs, or an interrupt exception is thrown, or time out error is throw in middle, all clean up will still take place.
  }catch(e){
     if(connection) connection.release();
  }
} 
Enter fullscreen mode Exit fullscreen mode

They have written the code as discussed in previous example (Problem 1), as may be thats what they wanted, as thats what the code does which they have written! (As it allows people to mess it up anyway even if await[timeOutInMs] is not there in place, as explianed in example 1).

This new syntax indeed gives a better control to developer to mandate him to wrap such a code with try catch:

try{
await[3000] doLongRunningTask();//a try catch as this line can possible throw timeout error or any other error within from function even without timeout
}catch(e){
//you actually get a chance to clean up manually if something goes wrong.
}
Enter fullscreen mode Exit fullscreen mode

Context

I was desining a consensus algorithm, where each particpant needs to send their response via websocket. As the response from each of the particpant can come anychrnously, the framework I use for websocketing provide this via asynchronous stream, which is then dealt by me using for await ...of loop.

for await(let data of asyncStreamFromChannelOfConsensusResponse){
//collect thier response
}
//do something after response are recived.
Enter fullscreen mode Exit fullscreen mode

Now this problem can't be left at the mercy of consensus participant, if one never sends any response, the code will run forever.
So a need for such an await with timeout aroused.

Its pretty simple, its very clear what it intends to do: await for x amount of timeat most === await[x] somePromise.
People have alwasy wanted cntrol on cancelling a promise (which they don't create) and this is one such way.

I hope that other people find it usefull and a good feature to have in lovely Javascript!

Comments are welcomed

Please raise issues or support with this new syntax.

Cheers!

Proposal in TC39 forum: https://es.discourse.group/t/timeout-for-an-async-loop-if-loop-do-not-finishes-before-timeout-it-will-break-anyway/1021
Here is the proposal:
https://github.com/anuragvohraec/proposal-es-await-timeout


Ok so the final verdict after discussion on TC39 Forum, on this issue is:
A better generalized syntax like this is already under propsal.

let someValue = await[cancelToken] getSomeValueFromPromise();
Enter fullscreen mode Exit fullscreen mode

Using cancel token to solve the timeout problem:

let someValue = await[timeoutFunc(3000)] getSomeValueFromPromise();
Enter fullscreen mode Exit fullscreen mode

Cancel token provides a more generalized approach for cancelling a promise.

let someValue = await[createCustomCancelToken(someArgs)] getSomeValueFromPromise();
Enter fullscreen mode Exit fullscreen mode

All this is under proposal and review so far, lets see if it is promoted futher in the JS syntax proposal lifecycle.

Discussion (0)