Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Timeout in async/await

I'm with Node.js and TypeScript and I'm using async/await. This is my test case:

async function doSomethingInSeries() {
    const res1 = await callApi();
    const res2 = await persistInDB(res1);
    const res3 = await doHeavyComputation(res1);
    return 'simle';
}

I'd like to set a timeout for the overall function. I.e. if res1 takes 2 seconds, res2 takes 0.5 seconds, res3 takes 5 seconds I'd like to have a timeout that after 3 seconds let me throw an error.

With a normal setTimeout call is a problem because the scope is lost:

async function doSomethingInSeries() {
    const timerId = setTimeout(function() {
        throw new Error('timeout');
    });

    const res1 = await callApi();
    const res2 = await persistInDB(res1);
    const res3 = await doHeavyComputation(res1);

    clearTimeout(timerId);

    return 'simle';
}

And I cannot catch it with normal Promise.catch:

doSomethingInSeries().catch(function(err) {
    // errors in res1, res2, res3 will be catched here
    // but the setTimeout thing is not!!
});

Any ideas on how to resolve?

like image 830
nkint Avatar asked May 09 '16 15:05

nkint


People also ask

Can we use async await with setTimeout?

With the help of Node. js development team, we are now able to use async/await syntax while dealing with setTimeout() functions. This feature was initially implemented in Node v.

Is set timeout async?

setTimeout() is an asynchronous function, meaning that the timer function will not pause execution of other functions in the functions stack. In other words, you cannot use setTimeout() to create a "pause" before the next function in the function stack fires.

How long does async await wait?

The keyword await is used to wait for a Promise. It can only be used inside an async function. This keyword makes JavaScript wait until that promise settles and returns its result. Here is an example with a promise that resolves in 2 seconds.

Can we use setTimeout in promise?

To make a promise from setTimeout with JavaScript, we can use the Promise constructor. const later = (delay) => { return new Promise((resolve) => { setTimeout(resolve, delay); }); }; to create the later function which returns a promise we create with the Promise constructor.

Does timeout need to be an async function?

@tinkerr "timeout needs to be declared async if it needs to be awaited" - Nope. A function only needs to return a promise that can be awaited (or actually, a thenable is enough). How it achieves that is up to the implementation of the function, it does not need to be an async function. @naisanza No, async / await is based on promises.

How can I await an object but with a timeout?

Say you have an awaitable object, and you want to await it, but with a timeout. How would you build that? What you can do is use a when_any -like function in combination with a timeout coroutine. For C# this would be something like await Task.WhenAny ( DoSomethingAsync (), Task.Delay (TimeSpan.FromSeconds (1)));

How to use setTimeout with ES7 async-await?

setTimeout is not an async function, so you can't use it with ES7 async-await. But you could implement your sleep function using ES6 Promise: function sleep (fn, par) { return new Promise ( (resolve) => { // wait 3s before calling fn (par) setTimeout ( () => resolve (fn (par)), 3000) }) }

How do I delay a task with a timeout coroutine?

What you can do is use a when_any -like function in combination with a timeout coroutine. For C# this would be something like await Task.WhenAny ( DoSomethingAsync (), Task.Delay (TimeSpan.FromSeconds (1))); The WhenAny method completes as soon as any of the passed-in tasks completes.


3 Answers

You can use Promise.race to make a timeout:

Promise.race([
    doSomethingInSeries(),
    new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 11.5e3))
]).catch(function(err) {
    // errors in res1, res2, res3 and the timeout will be caught here
})

You cannot use setTimeout without wrapping it in a promise.

like image 85
Bergi Avatar answered Oct 12 '22 13:10

Bergi


Ok I found this way:

async function _doSomethingInSeries() {
    const res1 = await callApi();
    const res2 = await persistInDB(res1);
    const res3 = await doHeavyComputation(res1);
    return 'simle';
}

async function doSomethingInSeries(): Promise<any> {
  let timeoutId;

  const delay = new Promise(function(resolve, reject){
    timeoutId = setTimeout(function(){
      reject(new Error('timeout'));
    }, 1000);
  });

  // overall timeout
  return Promise.race([delay, _doSomethingInSeries()])
    .then( (res) => {
      clearTimeout(timeoutId);
      return res;
    });

}

Anyone errors?

The things that smells a bit to me is that using Promises as asynchronicity strategy will send us to allocate too many object that some other strategy needs but this is off-topic.

like image 22
nkint Avatar answered Oct 12 '22 12:10

nkint


Problem with @Bergi answer that doSomethingInSeries continues executing even if you already rejected the promise. It is much better to cancel it.

LATEST ANSWER

You can try use AbortController for that. Check the old answer to see how to use it - api is similar.

Keep in mind that task is not cancelled immediately, so continuation (awaiting, then or catch) is not called exactly after timeout.

To guarantee that you can combine this and @Bergi approach.

OLD ANSWER

This is how it should look like:

async const doSomethingInSeries = (cancellationToken) => {
  cancellationToken.throwIfCancelled();

  const res1 = await callApi();

  cancellationToken.throwIfCancelled();

  const res2 = await persistInDB(res1);

  cancellationToken.throwIfCancelled();

  const res3 = await doHeavyComputation(res1);

  cancellationToken.throwIfCancelled();

  return 'simle';
}

Here is simple implementation:

const makeCancellationToken = (tag) => {
  let cancelled = false;

  return {
    isCancelled: () => cancelled,
    cancel: () => {
      cancelled = true;
    },
    throwIfCancelled: () => {
      if (cancelled) {
        const error = new Error(`${tag ?? 'Task'} cancelled`);
        error.cancelled = true;
        throw error;
      }
    }
  }
}

And finally usage:

const cancellationToken = makeCancellationToken('doSomething')

setTimeout(cancellationToken.cancel, 5000);

try {
  await doSomethingInSeries(cancellationToken);
} catch (error) {
  if (error.cancelled) {
    // handle cancellation
  }
}

Keep in mind that task is not cancelled immediately, so continuation (awaiting, then or catch) is not called exactly after 5 secs.

To guarantee that you can combine this and @Bergi approach.

like image 2
Alexander Danilov Avatar answered Oct 12 '22 12:10

Alexander Danilov