nkint
nkint

Reputation: 11733

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?

Upvotes: 40

Views: 49958

Answers (3)

gentlee
gentlee

Reputation: 3717

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.

Upvotes: 2

Bergi
Bergi

Reputation: 664599

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.

Upvotes: 67

nkint
nkint

Reputation: 11733

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.

Upvotes: 7

Related Questions