Retry failed promises using JavaScript

Retry failed promises using JavaScript

I recently found an exciting javascript problem online: "Retrying failed promises for specified n times". You have various libraries/utilities that do that but I wanted to try it with vanilla js.

Before we move on to solving this problem, let's first understand the question clearly.

  1. Retrying,

  2. Failed promises and,

  3. for given n times

let count = 0;

/**
 * functionThatReturnsAPromise
 *
 * @description Let's say it does some number increments and fails for the first 3 times and works fine from the 4th time.
 * @returns {Promise}
 */
const functionThatReturnsAPromise = () => {
  // A sample promise.
  // In the real world, this can be any promise,
  // a fetch api, a promise with heavy calculation, ...
  return new Promise((resolve, reject) => {
    if (count++ > 3) {
      resolve("Resolve message");
    } else {
      reject("Reject message");
    }
  });
};

Let's say this is our promise function and we need to retry this for n times when it fails. Let's write our utility function and let's call it retryPromise.

/**
 *
 * @param {Function} promiseFunction
 * @param {number} retriesCount
 * @returns {Promise}
 */
const retryPromise = (promiseFunction, retriesCount) => {
    // We are returning our wrapper promise.
    // But WHY?
    // This would help us to handle the retries internally and 
    // resolve (or) reject accordingly.
    // Just like currying, where we maintain an outer closure to 
    // check for the conditions and return accordingly.
  return new Promise((resolve, reject) => {
    // We have this inner function to narrow down the recursion to 
    // within this function itself. You could still take it to the outer function and recurse that but I feel this makes things simpler to understand.
    const innerFunction = () => {
    // We make a call to the promiseFunction.
    // NOTE: This function must return a promise (otherwise) it won't be useful to retry a normal function execution.
      promiseFunction()
        .then((data) => {
          // If it is resolved, then no need to retry.
          resolve(data);
        })
        .catch((err) => {
          // It failed. We need to retry for `retriesCount` times.
          if (retriesCount-- >= 0) {
            // As long as the retryCount value is `non-zero`, we keep calling the same innerFunction which repeats the same process over and over again.
            innerFunction();
          } else {
            // We reject it when the retryCount is zero.
            reject(err);
          }
        });
    };
    // Call this in the beginning to start the execution.
    innerFunction();
  });
};

Now, let's call this utility method by passing our promise function as an argument to it.

You can play around with the code in this codesandbox link - codesandbox.io/s/retry-promise-f7ep6j

// Using normal promise `then` / `catch` style.
const n = 2;
retryPromise(functionThatReturnsAPromise, n)
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.log(err);
  });

// This would result in "Reject message" getting printed on the console.

// Modifying the `n` value to anything above `2` would print "Resolve message" as the result

The same code can be run using async/await

try {
    const result = await retryPromise(functionThatReturnsAPromise, 3);
    console.log(result);
} catch (err) {
    console.log(err);
}

All good.

Now, let's say we have a promise method that makes an API (fetch, axios, ...) call. We would ideally want to wait a couple of seconds before retrying the same failed call.

But our utility would retry it continuously without any waiting. Hmm, ok then. Let's add some optional delays.

Now the same retryPromise will accept an optional 3rd argument.

const retryPromise = (promiseFunction, retriesCount, delayInMs) => {}
/**
 *
 * @param {Function} promiseFunction
 * @param {number} retriesCount
 * @param {number | undefined} delayInMs
 *
 * @returns {Promise}
 */
const retryPromise = (promiseFunction, retriesCount, delayInMs) => {
  return new Promise((resolve, reject) => {
    const innerFunction = () => {
      promiseFunction()
        .then((data) => {
          // If it is resolved, then no need to retry.
          resolve(data);
        })
        .catch((err) => {
          // It failed. We need to retry for `retriesCount` times.
          if (retriesCount-- >= 0) {
            // Checking if delay value is present
            // If non-zero, then we execute the timer,
            // otherwise, we directly execute the function.
            delayInMs
              ? setTimeout(() => {
                  innerFunction();
                }, delayInMs)
              : innerFunction();
          } else {
            reject(err);
          }
        });
    };
    innerFunction();
  });
};

That's all. Now you have a clean utility that you can use wherever required. And you can call the function with delay like below:

// delayInMs can be an optional param.
retryPromise(functionThatReturnsAPromise, 4, 1000)
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.log(err);
  });

Codesandbox link - https://codesandbox.io/s/retry-promise-f7ep6j

Cheers

Arunkumar Sri Sailapathi.