5

I have a function downloadItem that may fail for network reasons, I want to be able to retry it a few times before actually rejecting that item. The retries need to be with a timeout since if there is a network issue there is no point in retrying immediately.

Here is what I have so far:

function downloadItemWithRetryAndTimeout(url, retry, failedReason) {
    return new Promise(function(resolve, reject) {
        try {
            if (retry < 0 && failedReason != null) reject(failedReason);

            downloadItem(url);
            resolve();
        } catch (e) {
            setTimeout(function() {
                downloadItemWithRetryAndTimeout(url, retry - 1, e);
            }, 1000);
        }
    });
}

Obviously this will fail since the second (and on) time I call downloadItemWithRetryAndTimeout I don't return a promise as required.

How do I make it work correctly with that second promise?

P.S. incase it matters the code is running in NodeJS.

hippietrail
  • 15,848
  • 18
  • 99
  • 158
AlexD
  • 4,062
  • 5
  • 38
  • 65
  • maybe using promise does not fit your scenario. I don't get why people suddenly use promises for every single async case. – webduvet Aug 10 '15 at 14:31
  • I don't have to use a promise, you have any solution without a promise? – AlexD Aug 10 '15 at 14:38
  • 1
    @webduvet: because that's what promises are made for: async cases with a single result. And it does fit this scenario very well. – Bergi Aug 10 '15 at 16:29
  • 1
    @AlexD: Is your `downloadItem` function really synchronous? I think it should return a promise. – Bergi Aug 10 '15 at 16:29
  • 1
    @alexd with Bluebird you can use [Promise.delay](https://github.com/petkaantonov/bluebird/blob/master/API.md#delayint-ms---promise) and [Promise.try](https://github.com/petkaantonov/bluebird/blob/master/API.md#promisetryfunction-fn--arraydynamicdynamic-arguments--dynamic-ctx----promise) – aarosil Aug 11 '15 at 08:04
  • @Bergi, right, but if you need your result only once, and only in one case and you are not facing any nested callbacks problem, using promise library ads just unnecessary complexity. – webduvet Aug 11 '15 at 08:10

4 Answers4

5

I've got two ideas:

Move the promise out side of the iterated function downloadItemWithRetryAndTimeout - now resolve() will available to all iterations:

function downloadWrapper(url, retry) {
    return new Promise(function (resolve, reject) {
        function downloadItemWithRetryAndTimeout(url, retry, failedReason) {

            try {
                if (retry < 0 && failedReason != null)
                    reject(failedReason);

                downloadItem(url);
                resolve();
            } catch (e) {
                setTimeout(function () {
                    downloadItemWithRetryAndTimeout(url, retry - 1, e);
                }, 1000);
            }

        }

        downloadItemWithRetryAndTimeout(url, retry, null);
    });
}

This solution works, but it's an anti pattern as it breaks the promise chain: As each iteration returns a promise, just resolve the promise, and use .then to resolve the previous promise, and so on:

function downloadItemWithRetryAndTimeout(url, retry, failedReason) {
    return new Promise(function (resolve, reject) {
        try {
            if (retry < 0 && failedReason != null)
                reject(failedReason);

            downloadItem(url);
            resolve();
        } catch (e) {
            setTimeout(function () {
                downloadItemWithRetryAndTimeout(url, retry - 1, e).then(function () {
                    resolve();
                });
            }, 1000);
        }
    });
}
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
3

There is no need to create new promises to handle this. Assuming downloadItem is synchronous and returns a promise, simply return the result of calling it, along with a catch to call downloadItemWithRetryAndTimeout recursively.

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

function downloadItemWithRetryAndTimeout(url, retry) {
  if (retry < 0) return Promise.reject();

  return downloadItem(url) . 
    catch(() => wait(1000) . 
      then(() => downloadItemWithRetryAndTimeout(url, retry - 1)
  );
}

Some might find the following slightly cleaner:

function downloadItemWithRetryAndTimeout(url, retry) {
  return function download() {
    return --retry < 0 ? Promise.reject() :
      downloadItem(url) . catch(() => wait(1000) . then(download));
  }();
}
  • 2
    It'd make more sense to do it through a general retry method `const retry = (fn, retries = 3) => fn().catch(e => retries <= 0? Promise.reject(e) : retry(fn, retries - 1))` and compose it with timeouts: `const delay = ms => new Promise(r => setTimeout(r, ms))` and `const delayError = (fn, ms) => fn().catch(e => delay(ms).then(y => Promise.reject(e)))`. Then the code becomes `const downloadWithRetryAndTimeout = retry(delayError(download, 1000))` or something like that. – Benjamin Gruenbaum Mar 27 '16 at 15:10
  • @BenjaminGruenbaum Thanks for this elegant decomposition. –  Mar 28 '16 at 04:25
2

@BenjaminGruenbaum comment on @user663031 is fantastic but there's a slight error because this:

const delayError = (fn, ms) => fn().catch(e => delay(ms).then(y => Promise.reject(e)))

should actually be:

const delayError = (fn, ms) => () => fn().catch(e => delay(ms).then(y => Promise.reject(e)))

so it will return a function, not a promise. It's a tricky error that's hard to solve so I'm posting on here in case anyone needs it. Here's the whole thing:

const retry = (fn, retries = 3) => fn().catch(e => retries <= 0 ? Promise.reject(e) : retry(fn, retries - 1))
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
const delayError = (fn, ms) => () => fn().catch(e => delay(ms).then(y => Promise.reject(e)))
retry(delayError(download, 1000))
1
function downloadItemWithRetryAndTimeout(url, retry) {
    return new Promise(function(resolve, reject) {
        var tryDownload = function(attempts) {
            try {
                downloadItem(url);
                resolve();
            } catch (e) {
                if (attempts == 0)  {
                    reject(e);
                } else {
                    setTimeout(function() {
                        tryDownload(attempts - 1);
                    }, 1000);
                }
            }
        };
        tryDownload(retry);
    });
}
Rovak
  • 778
  • 1
  • 6
  • 13