How to retry a Promise resolution N times, with a delay between the attempts?

☆樱花仙子☆ 提交于 2020-12-06 06:38:17

问题


I want some JavaScript code to take 3 things as parameters:

  • A function returning a Promise.
  • The maximum number of attempts.
  • The delay between each attempt.

What I ended up doing is using a for loop. I did not want to use a recursive function : this way, even if there are 50 attempts the call stack isn't 50 lines longer.

Here is the typescript version of the code:

/**
 * @async
 * @function tryNTimes<T> Tries to resolve a {@link Promise<T>} N times, with a delay between each attempt.
 * @param {Object} options Options for the attempts.
 * @param {() => Promise<T>} options.toTry The {@link Promise<T>} to try to resolve.
 * @param {number} [options.times=5] The maximum number of attempts (must be greater than 0).
 * @param {number} [options.interval=1] The interval of time between each attempt in seconds.
 * @returns {Promise<T>} The resolution of the {@link Promise<T>}.
 */
export async function tryNTimes<T>(
    {
        toTry,
        times = 5,
        interval = 1,
    }:
        {
            toTry: () => Promise<T>,
            times?: number,
            interval?: number,
        }
): Promise<T> {
    if (times < 1) throw new Error(`Bad argument: 'times' must be greater than 0, but ${times} was received.`);
    let attemptCount: number;
    for (attemptCount = 1; attemptCount <= times; attemptCount++) {
        let error: boolean = false;
        const result = await toTry().catch((reason) => {
            error = true;
            return reason;
        });

        if (error) {
            if (attemptCount < times) await delay(interval);
            else return Promise.reject(result);
        }
        else return result;
    }
}

The delay function used above is a promisified timeout:

/**
 * @function delay Delays the execution of an action.
 * @param {number} time The time to wait in seconds.
 * @returns {Promise<void>}
 */
export function delay(time: number): Promise<void> {
    return new Promise<void>((resolve) => setTimeout(resolve, time * 1000));
}

To clarify: the code above works, I'm only wondering if this is a "good" way of doing it, and if not, how I could improve it.

Any suggestion? Thanks in advance for your help.


回答1:


I did not want to use a recursive function: this way, even if there are 50 attempts the call stack isn't 50 lines longer.

That's not a good excuse. The call stack doesn't overflow from asynchronous calls, and when a recursive solution is more intuitive than an iterative one you should probably go for it.

What I ended up doing is using a for loop. Is this a "good" way of doing it, and if not, how I could improve it?

The for loop is fine. It's a bit weird that it starts at 1 though, 0-based loops are much more idiomatic.

What is not fine however is your weird error handling. That boolean error flag should have no place in your code. Using .catch() is fine, but try/catch would work just as well and should be preferred.

export async function tryNTimes<T>({ toTry, times = 5, interval = 1}) {
    if (times < 1) throw new Error(`Bad argument: 'times' must be greater than 0, but ${times} was received.`);
    let attemptCount = 0
    while (true) {
        try {
            const result = await toTry();
            return result;
        } catch(error) {
            if (++attemptCount >= times) throw error;
        }
        await delay(interval)
    }
}



回答2:


Using recursive functions with Promises won't be an issue with the callstack as the Promise is returned instantly and the then or catch function will be called after an asynchronous event.

A simple javascript function would be like:

function wait (ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms)
  })
}

function retry (fn, maxAttempts = 1, delay = 0, attempts = 0) {
  return Promise.resolve()
    .then(fn)
    .catch(err => {
      if (attempts < maxAttempts) {
        return retry (fn, maxAttempts, delay, attempts + 1)
      }
      throw err
    })
}



回答3:


You might want to have a look at async-retry, which does exactly what you need. This package lets you retry async operations, and you can configure (among other things) timeouts between retries (even with increasing factors), maximum number of retries, …

This way you don't have to reinvent the wheel, but can rely on a proven package that is being widely used in the community.




回答4:


Have you considered RxJS?

It is excellent for implementing this sort of logic in async workflows.

Below is an example of how you would do this without breaking your public api (ie. converting from Promise to Observable and back). In practice you would probably want to use either RxJS or Promises in any given project rather than mixing them.

/**
 * @async
 * @function tryNTimes<T> Tries to resolve a {@link Promise<T>} N times, with a delay between each attempt.
 * @param {Object} options Options for the attempts.
 * @param {() => Promise<T>} options.toTry The {@link Promise<T>} to try to resolve.
 * @param {number} [options.times=5] The maximum number of attempts (must be greater than 0).
 * @param {number} [options.interval=1] The interval of time between each attempt in seconds.
 * @returns {Promise<T>} The resolution of the {@link Promise<T>}.
 */
export async function tryNTimes<T>(
    {
        toTry,
        times = 5,
        interval = 1,
    }:
        {
            toTry: () => Promise<T>,
            times?: number,
            interval?: number,
        }
): Promise<T> {
    if (times < 1) throw new Error(`Bad argument: 'times' must be greater than 0, but ${times} was received.`);
    let attemptCount: number;

    return from(toTry)
        .pipe(
            retryWhen(errors =>
                errors.pipe(
                    delay(interval * 1000),
                    take(times - 1)
                )
            )
        )
        .toPromise();
}

It may not be worth adding a whole library for this one piece of logic, but if your project involves a lot of complex async workflows such as this, then RxJS is great.



来源:https://stackoverflow.com/questions/55612859/how-to-retry-a-promise-resolution-n-times-with-a-delay-between-the-attempts

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!