Retry promises N number of times in JavaScript.

Implement a function in JavaScript that retries promises N number of times with a delay between each call.

Example

Input:
retry(asyncFn, retries = 3, delay = 50, finalError = 'Failed');

Output:
... attempt 1 -> failed
... attempt 2 -> retry after 50ms -> failed
... attempt 3 -> retry after 50ms -> failed
... Failed.

Promise retry design pattern in JavaScript

We have to create a retry function that Keeps on retrying until the promise resolves with delay and max retries.

We will see two code examples one with thenable promise and the second with async…await.

Delay function

We can create a delay function by creating a new promise and resolve it after a given time using setTimeout.

//delay func
const wait = ms => new Promise((resolve) => {
  setTimeout(() => resolve(), ms);
});

Using then…catch.

To retry the promise we have to call the same function recursively with reduced max tries, if the promise fails that is in the catch block. Check if there is a number of tries left then recursively call the same function or else reject with the final error.

const retryWithDelay = (
  operation, retries = 3, 
  delay = 50, finalErr = 'Retry failed') => new Promise((resolve, reject) => {
  return operation()
    .then(resolve)
    .catch((reason) => {
      //if retries are left
      if (retries > 0) {
        //delay the next call
        return wait(delay)
          //recursively call the same function to retry with max retries - 1
          .then(retryWithDelay.bind(null, operation, retries - 1, delay, finalErr))
          .then(resolve)
          .catch(reject);
      }
      
      // throw final error
      return reject(finalErr);
    });
});

To test we can create a test function that keeps throwing errors if called less than 5 times. So if we will call it 6 or more times, it will pass.

Input:
// Test function
const getTestFunc = () => {
  let callCounter = 0;
  return async () => {
    callCounter += 1;
    // if called less than 5 times
    // throw error
    if (callCounter < 5) {
      throw new Error('Not yet');
    }
  }
}

// Test the code
const test = async () => {
  await retryWithDelay(getTestFunc(), 10);
  console.log('success');
  await retryWithDelay(getTestFunc(), 3);
  console.log('will fail before getting here');
}

// Print the result
test().catch(console.error);

Output:
"success" // 1st test
"Retry failed" //2nd test

Using async…await.

We can implement the same login using async-await. When using async-await, we need to wrap the code inside try..catch block to handle the error, thus in the catch block, we can check if the max retries are still left then recursively call the same function or else throw the final error.

const retryWithDelay = async (
  fn, retries = 3, interval = 50,
  finalErr = 'Retry failed'
) => {
  try {
    // try
    await fn();
  } catch (err) {
    // if no retries left
    // throw error
    if (retries <= 0) {
      return Promise.reject(finalErr);
    }
    
    //delay the next call
    await wait(interval);
    
    //recursively call the same func
    return retryWithDelay(fn, (retries - 1), interval, finalErr);
  }
}
Input:
// Test function
const getTestFunc = () => {
  let callCounter = 0;
  return async () => {
    callCounter += 1;
    // if called less than 5 times
    // throw error
    if (callCounter < 5) {
      throw new Error('Not yet');
    }
  }
}

// Test the code
const test = async () => {
  await retryWithDelay(getTestFunc(), 10);
  console.log('success');
  await retryWithDelay(getTestFunc(), 3);
  console.log('will fail before getting here');
}

// Print the result
test().catch(console.error);

Output:
"success" // 1st test
"Retry failed" //2nd test

Alternatively, you can also update the code and keep on retrying the API call based on some tests.

Also checkout Circuit breaker in JavaScript