Search code examples
javascriptpromisetry-catches6-promise

Function to re-attempt a failed promise x times - final catch block never gets executed despite promise failing x times


I'm writing a function that will take a promise as an argument and if this promise rejects, the function will retry it x times before giving up. The issue I'm having is that after the promise rejects enough times, I get an exception due to not handling a rejected promise, and I haven't been able to figure out why.

My code is included at the bottom, and I've also made a codesandbox with my code. The logic is as follows:

  1. Call retry with the function, the amount of times to re-attempt and the delay in ms between each re-attempt.
  2. retry function attempts to execute the functionthat is passed to it.
  3. If it succeeds, resolve with the result of the function. If it fails, go to step 1 and decrease the attempts variable by 1.
  4. If the attempts variable is 0, retry rejects.

If retry rejects, it's then expected that the catch block in example() (line 61) is execute, but this never happens. Why does it never happen, and how can the code be changed to change this?

Edit: I've just noticed that the rejection isn't really clear in the codesandbox, but this is what happens when retry rejects:

node:internal/process/promises:279
            triggerUncaughtException(err, true /* fromPromise */);
            ^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "rejection".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

The full code, as requested:

const wait = (delay = 5000) =>
  new Promise((resolve) => setTimeout(resolve, delay));

const retry = ({ attempts, delay, fn, maxAttempts = attempts }) => {
  if (maxAttempts !== attempts) {
    /**
     * We want to double the delay on each
     * successive attempt after the first.
     */
    delay = delay * 2;
  }
  return new Promise(async (resolve, reject) => {
    try {
      console.log(1, `attempting ${fn.name}...`);
      const result = await fn();
      resolve(result);
    } catch (e) {
      if (attempts === 0) {
        console.log(2, `${fn.name} failed ${maxAttempts} times, giving up.`);
        reject(e);
        return;
      }
      console.log(
        2,
        `${fn.name} failed, retrying in ${
          delay / 1000
        }s... (${attempts} attempts remaining)`
      );
      await wait(delay);
      console.log(3, "retrying");
      retry({
        attempts: attempts - 1,
        delay: delay,
        fn,
        maxAttempts
      });
    }
  });
};

// An example promise that simply rejects after a second
const fakeReject = () =>
  new Promise((_, reject) => setTimeout(() => reject("rejection"), 1000));

const example = async () => {
  try {
    const result = await retry({
      attempts: 3,
      delay: 2000,
      fn: fakeReject
    });
    /**
     * The following would execute if fakeReject were to resolve instead.
     */
    console.log(result);
  } catch (e) {
    /**
     * I expected this block of code to execute once the fakeReject
     * promise has rejected 3 times, but this is never executed.
     */
    console.log("we tried");
  }
};

example()
  .then((x) => {
    console.log("success", x);
  })
  .catch(console.error);


Solution

  • Never pass an async function as the executor to new Promise! In your case you forgot to resolve the returned promise from the catch block of your code, you only made a recursive call - which constructs a new, independent promise that is ignored and whose rejections are never caught. It would have worked if you had written resolve(retry({…})), but really this is not needed:

    async function retry({ attempts, delay, fn, maxAttempts = attempts }) { /*
    ^^^^^ */
      if (maxAttempts !== attempts) {
        delay = delay * 2;
      }
      try {
        console.log(1, `attempting ${fn.name}...`);
        const result = await fn();
        return result;
    //  ^^^^^^
      } catch (e) {
        if (attempts === 0) {
          console.log(2, `${fn.name} failed ${maxAttempts} times, giving up.`);
          throw e;
    //    ^^^^^
        }
        console.log(2, `${fn.name} failed, retrying in ${delay / 1000}s... (${attempts} attempts remaining)`);
        await wait(delay);
        console.log(3, "retrying");
        return retry({
    //  ^^^^^^
          attempts: attempts - 1,
          delay: delay,
          fn,
          maxAttempts
        });
      }
    }