118

Edit

  1. Pattern that keep on retrying until the promise resolves (with delay and maxRetries).
  2. Pattern that keeps on retrying until the condition meets on the result (with delay and maxRetries).
  3. A memory efficient dynamic Pattern with unlimited retries (delay provided).

Code for #1. Keeps on retrying until promise resolves (any improvements community for the language etc?)

Promise.retry = function(fn, times, delay) {
    return new Promise(function(resolve, reject){
        var error;
        var attempt = function() {
            if (times == 0) {
                reject(error);
            } else {
                fn().then(resolve)
                    .catch(function(e){
                        times--;
                        error = e;
                        setTimeout(function(){attempt()}, delay);
                    });
            }
        };
        attempt();
    });
};

Use

work.getStatus()
    .then(function(result){ //retry, some glitch in the system
        return Promise.retry(work.unpublish.bind(work, result), 10, 2000);
    })
    .then(function(){console.log('done')})
    .catch(console.error);

Code for #2 keep on retrying until a condition meets on the then result in a reusable way (condition is what will vary).

work.publish()
    .then(function(result){
        return new Promise(function(resolve, reject){
            var intervalId = setInterval(function(){
                work.requestStatus(result).then(function(result2){
                    switch(result2.status) {
                        case "progress": break; //do nothing
                        case "success": clearInterval(intervalId); resolve(result2); break;
                        case "failure": clearInterval(intervalId); reject(result2); break;
                    }
                }).catch(function(error){clearInterval(intervalId); reject(error)});
            }, 1000);
        });
    })
    .then(function(){console.log('done')})
    .catch(console.error);
NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
user2727195
  • 7,122
  • 17
  • 70
  • 118
  • No sure what the `setInterval` will achieve inside promise, where is it resolving it? – ShuberFu Jul 05 '16 at 23:00
  • @jfriend, what happened to the answer, why it got deleted? – user2727195 Jul 05 '16 at 23:13
  • 1
    Don't add "edits" to your question. It makes it hard to follow. Instead, just edit your question. If someone wants to look at the edit history, then they can. –  Jul 06 '16 at 04:08
  • See http://stackoverflow.com/questions/37993365/retry-a-promise-step/37997151#37997151. –  Jul 06 '16 at 04:09
  • I'm thinking to delete this and start new. many things got clear during the process. – user2727195 Jul 06 '16 at 04:09
  • @torazaburo your answer are not what I need, they are using outside functions that are scattered around in the code. Please reopen, I'm trying to get to a pattern here, not any typical answer. also those answer do not improve my code in anyway. – user2727195 Jul 06 '16 at 04:13
  • @torazaburo please note the sophisticated and clean interface/use for Code #1 in my question – user2727195 Jul 06 '16 at 04:17
  • In code #2, if `collection.requestStatus(result)` rejects, you don't do anything - it will just loop forever. If it gets some sort of repeating error, you will never resolve or reject. If anything throws in your `collection.requestStatus(result).then()` handler, nobody captures that anywhere. Unexpected rejections should be propagated back so they can be handled. – jfriend00 Jul 06 '16 at 05:44
  • @jfriend00 great. Thanks for the help on this, I've fixed. all ok for #2 please? – user2727195 Jul 06 '16 at 05:47
  • Your failure to handle `.catch()` is the EXACT mistake that is often made when creating your own new promise rather than just chaining to a previous one. This is exactly why it is not recommended to create a new promise when you don't really need to. I've been in your spot before. You think this is simpler. But, it's not better (it is much more error prone) and once you get more comfortable with chaining, it's not even simpler. – jfriend00 Jul 06 '16 at 05:50
  • In #2, if you hit `.catch(reject)`, you don't stop your interval. – jfriend00 Jul 06 '16 at 06:00
  • In #2, you don't seem to do anything with the result of `.then(work.getStatus)`. I can't tell why it's even there. – jfriend00 Jul 06 '16 at 06:02
  • I realize what you're saying about chaining to previous ones, and I did experience the benefits chaining to existing promises while working on this project, I know you write me several attempts but hopefully I'm able to articulate now and you understand what I'm looking for, any edits to your answer please. and thanks for pointing out the interval error. yes it won't stop – user2727195 Jul 06 '16 at 06:02
  • you can ignore work.getStatus, it's an API call to basically get publishing status, a separate call after the publish, but you may ignore. I'm only interested in repetition in the most compact way without rolling my eyes else where. – user2727195 Jul 06 '16 at 06:04
  • 7
    This looks like an answer. Is there a question? – shoover Apr 25 '17 at 19:18
  • the later edits were my attempts to get to a solution actually – user2727195 Apr 25 '17 at 20:44
  • Really think this question needs to have the original question body at the top and edits, if any, added to the bottom. I don't know whether the code displayed is the question, the answer, attempts at an answer by the OP, or answers provided by the community added to the original. – Loz May 04 '23 at 14:15

23 Answers23

93

Something a bit different ...

Async retries can be achieved by building a .catch() chain, as opposed to the more usual .then() chain.

This approach is :

  • only possible with a specified maximum number of attempts. (The chain must be of finite length),
  • only advisable with a low maximum. (Promise chains consume memory roughly proportional to their length).

Otherwise, use a recursive solution.

First, a utility function to be used as a .catch() callback.

var t = 500;

function rejectDelay(reason) {
    return new Promise(function(resolve, reject) {
        setTimeout(reject.bind(null, reason), t); 
    });
}

Now you can build .catch chains very concisely :

1. Retry until the promise resolves, with delay

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).catch(rejectDelay);
}
p = p.then(processResult).catch(errorHandler);

DEMO: https://jsfiddle.net/duL0qjqe/

2. Retry until result meets some condition, without delay

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).then(test);
}
p = p.then(processResult).catch(errorHandler);

DEMO: https://jsfiddle.net/duL0qjqe/1/

3. Retry until result meets some condition, with delay

Having got your mind round (1) and (2), a combined test+delay is equally trivial.

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).then(test).catch(rejectDelay);
    // Don't be tempted to simplify this to `p.catch(attempt).then(test, rejectDelay)`. Test failures would not be caught.
}
p = p.then(processResult).catch(errorHandler);

test() can be synchronous or asynchronous.

It would also be trivial to add further tests. Simply sandwich a chain of thens between the two catches.

p = p.catch(attempt).then(test1).then(test2).then(test3).catch(rejectDelay);

DEMO: https://jsfiddle.net/duL0qjqe/3/


All versions are designed for attempt to be a promise-returning async function. It could also conceivably return a value, in which case the chain would follow its success path to the next/terminal .then().

Benny Code
  • 51,456
  • 28
  • 233
  • 198
Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • 1
    When you say, `only possible with a specified maximum number of attempts`, I can revert to `setInterval` based method for unlimited and do concatenation of promises within the `setInterval` method, would be nice to see your example for unlimited tries, if you don't mind. it will be used when I'm sure about the outcome whether `resolve` or `reject` but this procedure (publishing) takes too much time at times based on the file size. – user2727195 Jul 06 '16 at 20:26
  • `Promise chains consume memory roughly proportional to their length` but they are going to be released at the end of settlement? – user2727195 Jul 06 '16 at 20:28
  • I've edited #3 as unlimited retries, and I expect some memory efficient dynamic concatenation/chaining. – user2727195 Jul 06 '16 at 20:32
  • there's a bit of thing with your solution #2 and #3, `p.catch(attempt).then(test).catch(rejectDelay);`, basically if attempt resolves somewhere in the middle, all the following `then(test)` are executed to unwind the remaining chain I guess, the solution works for me but if you have something on "need to chain/dynamic" basis without complicating everything and keeping your existing solutions intact, it'd be great. – user2727195 Jul 06 '16 at 20:56
  • great work by the way, opened another dimension about promises – user2727195 Jul 06 '16 at 21:00
  • for #3, I've taken your approach and replace `for` loop with `setInterval` and clearing the `intervalId` on resolve/reject, so I believe this will continue to chain only if there's a need without complicating everything. For #1 and #2 i've to add an additional condition to check for `max` tries and `clearInterval` then. – user2727195 Jul 06 '16 at 21:09
  • nops, `setInterval` is not a solution in my attempts – user2727195 Jul 06 '16 at 21:22
  • Yes, forget about `setInterval()`. It has no place in a solution to your question. – Roamer-1888 Jul 06 '16 at 21:38
  • agree, is dynamic chaining possible based on the need? – user2727195 Jul 06 '16 at 21:50
  • "is dynamic chaining possible based on the need?" - no, this approach is totally predicated on building a chain then allowing to settle. – Roamer-1888 Jul 06 '16 at 22:03
  • Solutions #2 and #3 may indeed chain a number of `.then(test)` steps downstream of the first `.then(test)` that sees successfully returned data. By writing `test()` as in the demos, ie `return val` on passing the test, all downstream `.then(test)` steps will (assuming the test to be idempotent) result in re-passing the same test. Thoroughly logical but horribly inefficient especially if the tests are asynchronous. I'm sure that a mechanism could be contrived to avoid re-applying the same test over and over, but this approach would lose its simplicity. – Roamer-1888 Jul 06 '16 at 22:05
  • 8
    This whole solution path seems odd to me. Because it might retry up to N times, it pre-creates N objects in case they might be needed. If the actual operation succeeds in the first try, then not only did you create N-1 objects needlessly, but they then have to then be disposed of. It just seems conceptually inefficient and sometimes practically inefficient if N is anything but a small number. It also can't make arbitrary decisions about how many times to retry like a recursively chaining solution can. For example, it wouldn't know how to implement "retry automatically for up to 2 minutes". – jfriend00 Jul 08 '16 at 17:47
  • 4
    @user2727195 - I'm also not sure how the `test()` function can communicate back the difference between failure with retry to follow or failure with error to be communicated back and all retries aborted. Since a lot of the solutions have this structure `p.catch(attempt).catch(rejectDelay);`, there is no ability to reject in `attempt()` to abort further processing. This seems overly simplistic to a real world situation. There are usually failures that indicate a retry is desired and failures that indicate no further retries should be done and in fact this seems to be the case in the OP's code. – jfriend00 Jul 08 '16 at 17:52
55

2. Pattern that keeps on retrying until the condition meets on the result (with delay and maxRetries)

This is an nice way to do this with native promises in a recursive way:

const wait = ms => new Promise(r => setTimeout(r, ms));

const retryOperation = (operation, delay, retries) => new Promise((resolve, reject) => {
  return operation()
    .then(resolve)
    .catch((reason) => {
      if (retries > 0) {
        return wait(delay)
          .then(retryOperation.bind(null, operation, delay, retries - 1))
          .then(resolve)
          .catch(reject);
      }
      return reject(reason);
    });
});

This is how you call it, assuming that func sometimes succeeds and sometimes fails, always returning a string that we can log:

retryOperation(func, 1000, 5)
  .then(console.log)
  .catch(console.log);

Here we're calling retryOperation asking it to retry every second and with max retries = 5.

If you want something simpler without promises, RxJs would help with that: https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/retrywhen.md

Yair Kukielka
  • 10,686
  • 1
  • 38
  • 46
  • I like this approach but there is a bug when checking for remaining times to retry. It should be if (times > 0) {... – Piotr Lewandowski Oct 15 '20 at 12:07
  • I guess it depends on how you interpret the "times" you want to retry an operation. But if you take into account that the operation has already been run once before the catch, it is correct. – Yair Kukielka Oct 15 '20 at 15:12
  • I would expect the third parameter to be times or retries.. Try to execute retryOperation(func, 1000, 1) - the func will not be executed (retry) on failure. Would it make sense to call retryOperation(func, 1000, 0) and expect that the operation will not be called at all? - "0" parameter should mean: no retries - not 0 calls of the operation. – Piotr Lewandowski Oct 17 '20 at 11:26
  • well actuall retryOperation(func, 1000, 0) and retryOperation(func, 1000, 1) have the same effect - no retries – Piotr Lewandowski Oct 17 '20 at 11:30
44

There are many good solutions mentioned and now with async/await these problems can be solved without much effort.

If you don't mind a recursive approach then this is my solution.

function retry(fn, retries=3, err=null) {
  if (!retries) {
    return Promise.reject(err);
  }
  return fn().catch(err => {
      return retry(fn, (retries - 1), err);
    });
}
holmberd
  • 2,393
  • 26
  • 30
  • 1
    I like this simple aproach that stands the test of time. I would only add an error handler. – David Lemon Nov 27 '18 at 09:21
  • 1
    @DavidLemon yes most likely you would have a `catch()` on your `retry()` to provide you with latest error of the retries and the information that all of the retries failed. – holmberd Nov 27 '18 at 15:09
  • This was very helpful. I wanted to run `fn` until a desired output is achieved, or until the number of retries runs out, so I added a `.then()` before the `.catch()`. In my use case, the `.then()` makes the recursive call if needed, and the `.catch()` rejects on errors. – Marie Oct 17 '19 at 22:01
  • You might want to add a check for `fn` if it's a `function` or a promise object if you wish to pass parameters with the function. – Kristjan Kirpu Dec 15 '21 at 05:16
18

You can chain a new promise onto the prior one, thus delaying its eventual resolution until you know the final answer. If the next answer still isn't known, then chain another promise on it and keep chaining checkStatus() to itself until eventually you know the answer and can return the final resolution. That could work like this:

function delay(t) {
    return new Promise(function(resolve) {
        setTimeout(resolve, t);
    });
}

function checkStatus() {
    return work.requestStatus().then(function(result) {
        switch(result.status) {
            case "success":
                return result;      // resolve
            case "failure":
                throw result;       // reject
            case default:
            case "inProgress": //check every second
                return delay(1000).then(checkStatus);
        }
    });
}

work.create()
    .then(work.publish) //remote work submission
    .then(checkStatus)
    .then(function(){console.log("work published"})
    .catch(console.error);

Note, I also avoided creating the promise around your switch statement. Since you're already in a .then() handler, just returning a value is resolve, throwing an exception is reject and returning a promise is chaining a new promise onto the prior one. That covers the three branches of your switch statement without creating a new promise in there. For convenience, I do use a delay() function that is promise based.

FYI, this assumes the work.requestStatus() doesn't need any arguments. If it does need some specific arguments, you can pass those at the point of the function call.


It might also be a good idea to implement some sort of timeout value for how long you will loop waiting for completion so this never goes on forever. You could add the timeout functionality like this:

function delay(t) {
    return new Promise(function(resolve) {
        setTimeout(resolve, t);
    });
}

function checkStatus(timeout) {
    var start = Date.now();

    function check() {
        var now = Date.now();
        if (now - start > timeout) {
            return Promise.reject(new Error("checkStatus() timeout"));
        }
        return work.requestStatus().then(function(result) {
            switch(result.status) {
                case "success":
                    return result;      // resolve
                case "failure":
                    throw result;       // reject
                case default:
                case "inProgress": //check every second
                    return delay(1000).then(check);
            }
        });
    }
    return check;
}

work.create()
    .then(work.publish) //remote work submission
    .then(checkStatus(120 * 1000))
    .then(function(){console.log("work published"})
    .catch(console.error);

I'm not sure exactly what "design pattern" you're looking for. Since you seem to object to the externally declared checkStatus() function, here's an inline version:

work.create()
    .then(work.publish) //remote work submission
    .then(work.requestStatus)
    .then(function() {
        // retry until done
        var timeout = 10 * 1000;
        var start = Date.now();

        function check() {
            var now = Date.now();
            if (now - start > timeout) {
                return Promise.reject(new Error("checkStatus() timeout"));
            }
            return work.requestStatus().then(function(result) {
                switch(result.status) {
                    case "success":
                        return result;      // resolve
                    case "failure":
                        throw result;       // reject
                    case default:
                    case "inProgress": //check every second
                        return delay(1000).then(check);
                }
            });
        }
        return check();
    }).then(function(){console.log("work published"})
    .catch(console.error);

A more reusable retry scheme that could be used in many circumstances would define some reusable external code, but you seem to object to that so I haven't made that version.


Here's one other approach that uses a .retryUntil() method on the Promise.prototype per your request. If you want to tweak implementation details of this, you should be able to modify this general approach:

// fn returns a promise that must be fulfilled with an object
//    with a .status property that is "success" if done.  Any
//    other value for that status means to continue retrying
//  Rejecting the returned promise means to abort processing 
//        and propagate the rejection
// delay is the number of ms to delay before trying again
//     no delay before the first call to the callback
// tries is the max number of times to call the callback before rejecting
Promise.prototype.retryUntil = function(fn, delay, tries) {
    var numTries = 0;
    function check() {
        if (numTries >= tries) {
            throw new Error("retryUntil exceeded max tries");
        }
        ++numTries;
        return fn().then(function(result) {
            if (result.status === "success") {
                return result;          // resolve
            } else {
                return Promise.delay(delay).then(check);
            }
        });
    }
    return this.then(check);
}

if (!Promise.delay) {
    Promise.delay = function(t) {
        return new Promise(function(resolve) {
            setTimeout(resolve, t);
        });
    }
}


work.create()
    .then(work.publish) //remote work submission
    .retryUntil(function() {
        return work.requestStatus().then(function(result) {
            // make this promise reject for failure
            if (result.status === "failure") {
                throw result;
            }
            return result;
        })
    }, 2000, 10).then(function() {
        console.log("work published");
    }).catch(console.error);

I still can't really tell what you want or what about all these approaches is not solving your issue. Since your approaches seem to all be all inline code and not using a resuable helper, here's one of those:

work.create()
    .then(work.publish) //remote work submission
    .then(function() {
        var tries = 0, maxTries = 20;
        function next() {
            if (tries > maxTries) {
                throw new Error("Too many retries in work.requestStatus");
            }
            ++tries;
            return work.requestStatus().then(function(result) {
                switch(result.status) {
                    case "success":
                        return result;
                    case "failure":
                        // if it failed, make this promise reject
                        throw result;
                    default:
                        // for anything else, try again after short delay
                        // chain to the previous promise
                        return Promise.delay(2000).then(next);
                }

            });
        }
        return next();
    }).then(function(){
        console.log("work published")
    }).catch(console.error);
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • I liked the previous implementation, I solved it also somehow but don't like it, and same with your case, I liked your previous implementation which you deleted, and I liked it because of it's inline implementation within the then handler, the purpose is to come up with an elegant design pattern that works with then handler without the need of outsider functions and rather than just solve the issue, can we please rework – user2727195 Jul 05 '16 at 23:24
  • @user2727195 - The previous implementation did not work - that's why I changed it. It did not loop until result found. It just called `work.requestStatus()` one more time and had no ability to loop. I believe this meets the requirements of your question. If you are looking for something beyond this, then please edit your question to specify what else you are looking for and leave me a comment to tell me you've done so. – jfriend00 Jul 05 '16 at 23:34
  • 1
    @user2727195 - The `checkStatus()` implementation can be done inline without a separately named function if that is somehow bothering you, but I thought it was a cleaner implementation to break it out as I did because it makes the `.then().then().then()` chain a lot simpler to follow and see exactly what is going on there. Also, I needed to create a closure for the timeout management and this was a convenient way to do so without introducing any variables at the top scope level. – jfriend00 Jul 05 '16 at 23:38
  • the point is to have a reusable pattern, I mean I have the same need to unpublish and criteria is different, and same with delete. A reusable design pattern if we can develop. In previous implementation I liked how you were concatenating promises (if i remember correctly) and then on success it resolved back. otherwise If I keep on writing additional custom code then it become laborious. Edited my question as well, i hope it clears up – user2727195 Jul 05 '16 at 23:44
  • 1
    @user2727195 - This could likely be made more generic. Please edit your question to describe exactly what criteria you are looking for. You vaguely mention "design pattern", but are not very specific about what would actually meet your requirements. – jfriend00 Jul 05 '16 at 23:46
  • let me try again, you know how we formed chains with all the `then` handlers using promises, what if we can chain the retry handlers inside without the need of defining additional functions, something on the lines of adding `then`s to internal retry promises. – user2727195 Jul 05 '16 at 23:48
  • @user2727195 - A reusable retry pattern that is simple to use for retry in many circumstances will require an external function that implements the generic code you are going to reuse. Is that allowed? Your requirements are far too vague for me to know what you're looking for. – jfriend00 Jul 05 '16 at 23:48
  • maybe up to delay function is fine, but then can retry promises by chained? – user2727195 Jul 05 '16 at 23:50
  • for instance dominic wrote this one, but there's no delay. https://gist.github.com/domenic/2936696 – user2727195 Jul 05 '16 at 23:51
  • @user2727195: You can trivially move the `checkStatus` function into the `then` chain as jfriend explained above. It works just as well as a function expression as it does as the declaration. – Bergi Jul 05 '16 at 23:52
  • @user2727195 - I added an inline version. – jfriend00 Jul 05 '16 at 23:54
  • 1
    @user2727195 - You can't repeat code indefinitely asynchronously without having some function that you can call repeatedly. That's why `check()` is defined as a function. It's how you make a body of code that you can repeatedly call asynchronously. – jfriend00 Jul 05 '16 at 23:56
  • @user2727195 - I'd personally be more interested in some reusable code that helps you do generic promise retry. Since your completion logic is custom for this case, that generic code would need to accept a callback that could run the operation and check it's result and return success, failure or still going. But you seem to be averse to externally defined functions so I haven't gone that direction. – jfriend00 Jul 05 '16 at 23:58
  • yes, that's what I'm looking for too, some generic promise retry, maybe define one the Promise.prototype, – user2727195 Jul 05 '16 at 23:59
  • @user2727195 - As I've asked multiple times, please edit your question to describe what you want. It is very frustrating to answer the question you think was asked, then be told that isn't really what you meant to ask and then find unclear and evolving requirements. It's a moving target, very hard to hit with an answer. When I feel like I understand what you want from the words of your question (not just the accumulation of your comments), I may take one more shot at a more generic solution. But, I will wait until then. – jfriend00 Jul 06 '16 at 00:01
  • maybe this will help, https://github.com/icodeforlove/promise-retryer, instead of using library, something defined on the Promise.prototype, that will take callback, delay and max retries, and resolve, reject. – user2727195 Jul 06 '16 at 00:06
  • @user2727195 - I added one last implementation using a generic `.retryUntil()` on the Promise prototype. The challenge with the callback that `retryUntil()` takes is that it returns a promise that has to communicate three states (done, not done yet, error). I chose to have it resolve with an object that has a `.status` property, though there are many ways to do that. The way I chose allows you to resolve with lots of other data besides just the `.status` value which seemed require in some circumstances, but this could be structured differently too. – jfriend00 Jul 06 '16 at 00:19
  • @user2727195 - So what do you think of my `.retryUntil()`? You were very communicative before I posted it and now your are suddenly silent. – jfriend00 Jul 06 '16 at 01:30
  • @jfriend00 please see my answer, perhaps with code the intention is much clear, now if you will, you may cleanup your slate for any revisions from my code you think may fit, and I'll accept your answer. – user2727195 Jul 06 '16 at 03:52
  • @user2727195 - You've come up with a few ideas on how to do this. I've come up with a few ideas on how to do it. You didn't offer a specification for exactly what you wanted so these are all just ideas that could all work. I like chaining existing promises as a general promise design pattern better than creating a new wrapping promise for a variety of reasons related to how promises work and the likelihood that one might suffer from programming mistakes. For example, each of your examples is missing throw safety in some spots. – jfriend00 Jul 06 '16 at 04:32
  • @jfriend00 agree, chaining existing promises is the best and they are most compact solutions without bringing noise in the code. Yes my specification was not clear as things were in my head only, I was only to articulate only what I want after I kept on repeating attempts. – user2727195 Jul 06 '16 at 05:34
  • And guess what I'm sticking with `setInterval` (Code #2) approach for both. Basically I'm handling both "retry promise until resolve" and "retry promise with condition on result" within a `setInterval` block within `then` handler, and then progressing only after resolve or rejection after clearing intervals. That's a compact pattern would be for me without rolling my eyes else where. I hope you know what I mean. I'd accept your attempt. Appreciate your help. I'm dropping Code #1 for any further attempts and continuing with code#2 approach whenever i need repetition. – user2727195 Jul 06 '16 at 05:35
  • I'm interested to know throw safety spots, please mention fix any in my code#2 approach, I think any error in `then` handlers are automatically taken to `reject` handlers – user2727195 Jul 06 '16 at 05:39
  • @jfriend00 I guess the articulated question is, how can I keep on retrying until a condition meets on the `then` result in a reusable way (condition is what will vary) while using existing promise chains and not creating new ones. I'll ponder on it and you can too if you will, if you think you can workout in few days let me know or I can close this with acceptance to answer here. – user2727195 Jul 06 '16 at 06:29
  • @user2727195 - I added one more option that uses inline code. I'm out of other ideas so please either describe what is missing from this answer or finish off the question in some way. – jfriend00 Jul 08 '16 at 16:14
  • @jfriend00 this helped me a lot in refining what I was looking for. https://www.youtube.com/watch?v=lil4YCCXRYc a combination of AsyncIterator and Observer pattern – user2727195 Jul 29 '16 at 02:59
14

Here is an "exponential backoff" retry implementation using async/await that can wrap any promise API.

note: for demonstration reasons snippet simulates a flaky endpoint with Math.random, so try a few times to see both success and failure cases.

/**
 * Wrap a promise API with a function that will attempt the promise 
 * over and over again with exponential backoff until it resolves or
 * reaches the maximum number of retries.
 *   - First retry: 500 ms + <random> ms
 *   - Second retry: 1000 ms + <random> ms
 *   - Third retry: 2000 ms + <random> ms
 * and so forth until maximum retries are met, or the promise resolves.
 */
const withRetries = ({ attempt, maxRetries }) => async (...args) => {
  const slotTime = 500;
  let retryCount = 0;
  do {
    try {
      console.log('Attempting...', Date.now());
      return await attempt(...args);
    } catch (error) {
      const isLastAttempt = retryCount === maxRetries;
      if (isLastAttempt) {
        // Stack Overflow console doesn't show unhandled
        // promise rejections so lets log the error.
        console.error(error);
        return Promise.reject(error);
      }
    }
    const randomTime = Math.floor(Math.random() * slotTime);
    const delay = 2 ** retryCount * slotTime + randomTime;
    // Wait for the exponentially increasing delay period before 
    // retrying again.
    await new Promise(resolve => setTimeout(resolve, delay));
  } while (retryCount++ < maxRetries);
}

const fakeAPI = (arg1, arg2) => Math.random() < 0.25 
  ? Promise.resolve(arg1) 
  : Promise.reject(new Error(arg2))
  
const fakeAPIWithRetries = withRetries({ 
  attempt: fakeAPI, 
  maxRetries: 3 
});

fakeAPIWithRetries('arg1', 'arg2')
 .then(results => console.log(results))
Red Mercury
  • 3,971
  • 1
  • 26
  • 32
  • I left a suggested edit lowering the success rate from 1-in-4 to 1-in-5. Previously, with four attempts each having a 1/4 chance of success, it added up to a _very_ strong likelihood of success for every run. – FeRD Apr 16 '22 at 03:56
  • (Well, I _thought_ I did, but now I don't see it. Still, I'd suggest replacing `Math.random() < 0.25` with `Math.random() < 0.2`, to make rejections more likely. With the current code, you can go several runs without hitting a rejection. It's not quite as bad as I thought (I was adding instead of multiplying), but the combined failure probability is still `0.75^4`, or around `0.32`. Raising it to `0.8^4` gets you `~0.41`.) – FeRD Apr 17 '22 at 17:23
  • "that can wrap any promise API" - async/await cannot wrap any promise API that relies on monkey patching: rewriting how Promise works. "can wrap up pretty much any promise API" +1 – TamusJRoyce Jul 05 '22 at 21:33
10

Check @jsier/retrier. Tested, documented, lightweight, easy-to-use, without external dependencies and already in production for quite some time now.

Supports:

  • First attempt delay
  • Delay between attempts
  • Limiting number of attempts
  • Callback to stop retrying if some condition is met (e.g. specific error is encountered)
  • Callback to keep retrying if some condition is met (e.g. resolved value is unsatisfactory)

Installation:

npm install @jsier/retrier

Usage:

import { Retrier } from '@jsier/retrier';

const options = { limit: 5, delay: 2000 };
const retrier = new Retrier(options);
retrier
  .resolve(attempt => new Promise((resolve, reject) => reject('Dummy reject!')))
  .then(
    result => console.log(result),
    error => console.error(error) // After 5 attempts logs: "Dummy reject!"
  );

The package has no external dependencies.

seidme
  • 12,543
  • 5
  • 36
  • 40
8

Here's my attempt. I tried to take what I liked from all of the above answers. No external dependencies. Typescript + async / await (ES2017)

export async function retryOperation<T>(
  operation: () => (Promise<T> | T), delay: number, times: number): Promise<T> {
    try {
      return await operation();
    } catch (ex) {
      if (times > 1) {
        await new Promise((resolve) => setTimeout(resolve, delay));
        return retryOperation(operation, delay, times - 1);
      } else {
        throw ex;
      }
    }
}

Usage:

function doSomething() {
  return Promise.resolve('I did something!');
}

const retryDelay = 1000; // 1 second
const retryAttempts = 10;


retryOperation(doSomething, retryDelay, retryAttempts)
    .then((something) => console.log('I DID SOMETHING'))
    .catch((err) => console.error(err));
Bryan McGrane
  • 498
  • 6
  • 14
7

Building on the solution by holmberd with a little cleaner code and a delay

// Retry code

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


const retryWithDelay = async (
  fn, retries = 3, interval = 50,
  finalErr = Error('Retry failed')
) => {
  try {
    await fn()
  } catch (err) {
    if (retries <= 0) {
      return Promise.reject(finalErr);
    }
    await wait(interval)
    return retryWithDelay(fn, (retries - 1), interval, finalErr);
  }
}

// Test

const getTestFunc = () => {
  let callCounter = 0
  return async () => {
    callCounter += 1
    if (callCounter < 5) {
      throw new Error('Not yet')
    }
  }
}

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


test().catch(console.error)
i4h
  • 851
  • 7
  • 8
  • Hi! Tried this, cant make it work at all, wont execute. Too clever for me, cant see what is wrong. Is it tested? – Marcus Widerberg Sep 15 '20 at 14:25
  • 1
    Hey, thanks for the comment @MarcusWiderberg, there was an error in there. I updated the solution and added a test so now you can copy-paste and run the whole script and see how it works – i4h Mar 21 '21 at 08:47
  • This is good but you need to remove the immediate execution of the getTestFunc. It should be instead: `await retryWithDelay(getTestFunc, 10)` – dalcam Oct 05 '22 at 13:26
6

If your code is placed in a class you could use a decorator for that. You have such decorator in the utils-decorators (npm install --save utils-decorators) lib:

import {retry} from 'utils-decorators';

class SomeService {

   @retry(3)
   doSomeAsync(): Promise<any> {
    ....
   }
}

or you could use a wrapper function:

import {retryfy} from 'utils-decorators';

const withRetry = retryfy(originalFunc, 3);

Note: this lib is tree shakable so you won't pay extra bytes for the rest of the available decorators in this lib.

https://github.com/vlio20/utils-decorators#retry-method

vlio20
  • 8,955
  • 18
  • 95
  • 180
4

There are plenty answers here, but after some research i decided to go with a recursive approach. Im leaving my solution here for any one interested

function retry(fn, retriesLeft = 2, interval = 1000) {
  return new Promise((resolve, reject) => {
    fn()
      .then(resolve)
      .catch((error) => {
        if (retriesLeft === 0) {
          reject(error);
          return;
        }

        setTimeout(() => {
          console.log('retrying...')
          retry(fn, retriesLeft - 1, interval).then(resolve).catch(reject);
        }, interval);
      });
  });
}

Here is a stackblitz with a nice playground where you can get the feel on how it works. Just play around the intent variable to see the promise resolve/reject

https://js-vjramh.stackblitz.io

Guido Dizioli
  • 2,007
  • 2
  • 17
  • 29
4

Not sure why all the solutions proposed are recursive. An iterative solution with TypeScript that waits until the method returns something that is not undefined:

function DelayPromise(delayTime): Promise<void> {
  return new Promise<void>((resolve) => setTimeout(resolve, delayTime));
}

interface RetryOptions {
  attempts?: number;
  delayMs?: number;
}

export async function retryOperation<T>(
  operation: (attempt: number) => Promise<T>,
  options: RetryOptions = {}
): Promise<T> {
  const { attempts = 6, delayMs = 10000 } = options;
  for (let i = 0; i < attempts; i++) {
    const result = await operation(i);
    if (typeof result !== 'undefined') {
      return result;
    }
    await DelayPromise(delayMs);
  }
  throw new Error('Timeout');
}
Nacho Coloma
  • 7,070
  • 2
  • 40
  • 43
1

async-retry.ts is trying to implement the pattern, I'm using it in production for some projects.

Installation:

npm install async-retry.ts --save

Usage:

import Action from 'async-retry.ts'
 
const action = async()=>{}
const handlers = [{
  error: 'error1',
  handler: async yourHandler1()=>{}
}, {
  error: 'error2',
  handler: async yourHandler2()=>{}
}]
 
await Action.retryAsync(action, 3, handlers)

This package is quite new but it is derived from a long lived package co-retry which implemented the retry pattern in generator function fashion.

Jeff Tian
  • 5,210
  • 3
  • 51
  • 71
1
function TryToSuccess(fun, reties) {
    let attempt = 0;

    let doTry = (...args) => {
        attempt++;
        return fun(...args)
                .catch((err) => {
                    console.log("fail ", attempt);
                    if(attempt <= reties){
                        return doTry(...args);
                    } else {
                        return Promise.reject(err);
                    }
                });
    }

    return doTry;
}

function asyncFunction(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            (window.findResult === true) ? resolve("Done") : reject("fail");
        }, 2000);
    });
}

var cloneFunc = TryToSuccess(asyncFunction, 3);

cloneFunc()
    .then(res => { 
        console.log("Got Success. ", res)
    })
    .catch(err => { 
        console.log("Rejected with err ", err); 
    });

setTimeout(() => {
    window.findResult = true;
}, 4000);
  • While this code may answer the question, providing additional context regarding how and/or why it solves the problem would improve the answer's long-term value. – ItsPete Nov 27 '19 at 03:47
1

Here is my solution:

  • Preserve function type using Typescript.
  • Accept a function with any parameters.
  • Customize number of maxRetries.
  • Customize delay behavior
type AnyFn = (...any: any[]) => any;
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
type DelayFn = (retry: number) => number;

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export function retry<Fn extends AnyFn>(
  fn: Fn,
  maxRetries: number,
  getDelay: DelayFn = () => 5000
) {
  let retries = 0;

  return async function wrapped(
    ...args: Parameters<Fn>
  ): Promise<Awaited<ReturnType<Fn>>> {
    try {
      return await fn(...args);
    } catch (e) {
      if (++retries > maxRetries) throw e;

      const delayTime = getDelay(retries);
      console.error(e);
      console.log(`Retry ${fn.name} ${retries} times after delaying ${delayTime}ms`);
      await delay(delayTime);
      return await wrapped(...args);
    }
  };
}

Usage

const badFn = () => new Promise((resolve, reject) => reject('Something is wrong');
const fn = retry(badFn, 5, (retry) => 2 ** retry * 1000);

fn();

// Something is wrong
// Retry badFn 1 times after delaying 2000ms
// Something is wrong
// Retry badFn 2 times after delaying 4000ms
// Something is wrong
// Retry badFn 3 times after delaying 8000ms
// Something is wrong
// Retry badFn 4 times after delaying 16000ms
// Something is wrong
// Retry badFn 5 times after delaying 32000ms

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

function retry(fn, maxRetries, getDelay = () => 5000) {
  let retries = 0;

  return async function wrapped(...args) {
    try {
      return await fn(...args);
    } catch (e) {
      if (++retries > maxRetries) throw e;
      const delayTime = getDelay(retries);
      console.error(e);
      console.log(`Retry ${fn.name} ${retries} times after delaying ${delayTime}ms`);
      await delay(delayTime);
      return await wrapped(...args);
    }
  };
}

const badFn = () => new Promise((resolve, reject) => reject('Something is wrong'));
const fn = retry(badFn, 5, (retry) => 2 ** retry * 1000);

fn();
NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
0
work.create()
    .then(work.publish) //remote work submission
    .then(function(result){
        var maxAttempts = 10;
        var handleResult = function(result){
            if(result.status === 'success'){
                return result;
            }
            else if(maxAttempts <= 0 || result.status === 'failure') {
                return Promise.reject(result);
            }
            else {
                maxAttempts -= 1;
                return (new Promise( function(resolve) {
                    setTimeout( function() {
                        resolve(_result);
                    }, 1000);
                })).then(function(){
                    return work.requestStatus().then(handleResult);
                });
            }
        };
        return work.requestStatus().then(handleResult);
    })
    .then(function(){console.log("work published"})
    .catch(console.error);
Hugo Silva
  • 6,748
  • 3
  • 25
  • 42
  • Avoid the promise constructor antipattern! – Bergi Jul 05 '16 at 23:50
  • @Bergi - I can't figure out how to resolve this in a more readable way. Could you please shed some light? Also, would please you scroll to the bottom of this article (https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns#the-deferred-anti-pattern), where it talks about `setTimeout`, and make sure this scenario doesn't fit into the exception? – Hugo Silva Jul 06 '16 at 00:45
  • Have a look at jfriend's answer :-) `setTimeout` does fit the exception, so use the `Promise` constructor to get a promise for the delay, but `requestStatus` does return a promise and using it insde the `Promise` constructor is the antipattern. – Bergi Jul 06 '16 at 00:49
  • In this case, I think I prefer the so called anti pattern, for it results in code that is more readable and easier to follow. Thanks for pointing it out though, made me acquire some valuable information. – Hugo Silva Jul 06 '16 at 00:58
  • …[and more likely to break](http://stackoverflow.com/a/25569299/1048572). It's not even simpler if you are accustomed to monads. – Bergi Jul 06 '16 at 01:01
  • I don't know... I am not convinced on this particular case. But, for the learning experience, I have edited the code. Would you look at it, and let me know your thoughts? Note that I removed one step from the promises chain, which I think was causing part of the confusion. – Hugo Silva Jul 06 '16 at 01:50
  • Looks better, thank you. You're missing a `return` before `work.requestStatus()…` and one before `Promise.reject(result)` (or you should `throw result`), and you meant `maxAttempts` instead of `currentAttempts` – Bergi Jul 06 '16 at 02:25
0

One library can do this easily : promise-retry.

Here are some examples to test it :

const promiseRetry = require('promise-retry');

Expect second attempt to be successful :

it('should retry one time after error', (done) => {
    const options = {
        minTimeout: 10,
        maxTimeout: 100
    };
    promiseRetry((retry, number) => {
        console.log('test2 attempt number', number);
        return new Promise((resolve, reject) => {
            if (number === 1) throw new Error('first attempt fails');
            else resolve('second attempt success');
        }).catch(retry);
    }, options).then(res => {
        expect(res).toBe('second attempt success');
        done();
    }).catch(err => {
        fail(err);
    });
});

Expect only one retry :

it('should not retry a second time', (done) => {
    const options = {
        retries: 1,
        minTimeout: 10,
        maxTimeout: 100
    };
    promiseRetry((retry, number) => {
        console.log('test4 attempt number', number);
        return new Promise((resolve, reject) => {
            if (number <= 2) throw new Error('attempt ' + number + ' fails');
            else resolve('third attempt success');
        }).catch(retry);
    }, options).then(res => {
        fail('Should never success');
    }).catch(err => {
        expect(err.toString()).toBe('Error: attempt 2 fails');
        done();
    });
});
Bludwarf
  • 824
  • 9
  • 21
0

My solution for TypeScript:

export const wait = (milliseconds: number): Promise<void> =>
  new Promise(resolve => {
    setTimeout(() => resolve(), milliseconds);
  });

export const retryWithDelay = async (
  fn,
  retries = 3,
  interval = 300
): Promise<void> =>
  fn().catch(async error => {
    if (retries <= 0) {
      return Promise.reject(error);
    }
    await wait(interval);
    return retryWithDelay(fn, retries - 1, interval);
  });

Based on solutions above, fixed milliseconds for wait since it would default to 50 seconds instead of ms and now throws the error that caused the failure instead of a hardcoded mesasge.

SeedyROM
  • 2,203
  • 1
  • 18
  • 22
0

I give you an async/await solution, have fun with it :)

async function scope() {

  /* Performs an operation repeatedly at a given frequency until
     it succeeds or a timeout is reached and returns its results. */
  async function tryUntil(op, freq, tout) {
    let timeLeft = tout;
    while (timeLeft > 0) {
      try {
        return op();
      } catch (e) {
        console.log(timeLeft + " milliseconds left");
        timeLeft -= freq;
      }
      await new Promise((resolve) => setTimeout(() => resolve(), freq));
    }
    throw new Error("failed to perform operation");
  }

  function triesToGiveBig() {
    const num = Math.random();
    if (num > 0.95) return num;
    throw new Error();
  }

  try {
    console.log(await tryUntil(triesToGiveBig, 100, 1000));
  } catch (e) {
    console.log("too small :(");
  }

}

scope();
tom
  • 2,137
  • 2
  • 27
  • 51
0

Just in case somebody is looking for a more generic solution. Here are my two cents:

Helper Function:

/**
 * Allows to repeatedly call
 * an async code block
 *
 * @callback callback
 * @callback [filterError] Allows to differentiate beween different type of errors
 * @param {number} [maxRetries=Infinity]
 */
function asyncRetry(
  callback,
  { filterError = (error) => true, maxRetries = Infinity } = {}
) {
  // Initialize a new counter:
  let tryCount = 0;
  // Next return an async IIFY that is able to
  // call itself recursively:
  return (async function retry() {
    // Increment out tryCount by one:
    tryCount++;
    try {
      // Try to execute our callback:
      return await callback();
    } catch (error) {
      // If our callback throws any error lets check it:
      if (filterError(error) && tryCount <= maxRetries) {
        // Recursively call this IIFY to repeat
        return retry();
      }
      // Otherwise rethrow the error:
      throw error;
    }
  })();
}

Demo

Try 2 times:

await asyncRetry(async () => {
  // Put your async code here
}, { maxRetries = 2 })

Try 2 times & only retry on DOMErrors:

await asyncRetry(async () => {
  // Put your async code here
}, { 
  maxRetries = 2,
  filterError: (error) => error instance of DOMError
})

Infine Retry: (Don't do this!)

await asyncRetry(async () => {
  // Put your async code here
})
HaNdTriX
  • 28,732
  • 11
  • 78
  • 85
0

Simple Promise Retry :

function keepTrying(otherArgs, promise) {
    promise = promise||new Promise();
    
    // try doing the important thing
    
    if(success) {
        promise.resolve(result);
    } else {
        setTimeout(function() {
            keepTrying(otherArgs, promise);
        }, retryInterval);
    }
}
Jabbar Memon
  • 83
  • 1
  • 5
0

This works perfectly for me:

async wait(timeInMilliseconds: number, name?: string) {
    const messageSuffix = name ? ` => ${name}` : ""
    await this.logger.info(`Waiting for ${timeInMilliseconds} ms${messageSuffix}`).then(log => log())
    return new Promise<void>(resolve => setTimeout(resolve, timeInMilliseconds))
}

async waitUntilCondition(name: string, condition: () => boolean | Promise<boolean>, scanTimeInSeconds: number, timeoutInSeconds: number) {
    await this.logger.info(`Waiting until condition: name=${name}, scanTime: ${scanTimeInSeconds} s, timeout: ${timeoutInSeconds} s`).then(log => log())
    const timeoutInMillis = timeoutInSeconds * 1000
    return new Promise<void>(async (resolve, reject) => {
        const startTime = new Date().getTime()
        let completed = false
        let iteration = 0
        while (!completed) {
            if (iteration++ > 0) {
                const timingOutInSeconds = Math.round((timeoutInMillis - (new Date().getTime() - startTime)) / 1000.0)
                await this.wait(scanTimeInSeconds * 1000, `${name}, timing out in ${timingOutInSeconds} s`)
            }
            try {
                completed = await condition()
                if (completed) {
                    resolve()
                    return
                }
            } catch (error: any) {
                reject(error)
                throw error
            }
            const waitTimeMillis = new Date().getTime() - startTime
            if (waitTimeMillis > timeoutInMillis) {
                reject(`The condition '${name}' timed out. Time waited: ${waitTimeMillis / 1000} seconds`)
                return
            }
        }
    })
}
0

An approach with control on if the operation can indeed be retried

In practice, I found that we shouldn't assume that the operation is always retryable, and the generic retry helper needs a way to delegate that check to the higher level calling code. The example below shows what worked for me.


/* The retry function takes a function to invoke, and a set 
 * of optional parameters to control the delay between retries 
 * (no backoff algorithm implemented here, but other example 
 * show how you might add that one), how many times to attempt
 * retrying and also a way to check if a retry should be 
 * attempted.
 *
 * And it returns a Promise that can be used in promise-
 * chaining and other async patterns.
 *
 */
const retry = (fn, 
               ms = 1000, 
               maxRetries = 2, 
               fnRetryable) => new Promise((resolve, reject) => {

  var retries = 0;

  if(!fnRetryable) {
    // default to always retryable
    fnRetryable = function() { return true };
  }

  fn()
  .then(resolve)
  .catch((err) => {
    if(!fnRetryable(err)) {
      return reject('Non-retryable');
    } else {
      setTimeout(() => {
        ++retries;
        if(retries == maxRetries) {
          return reject('Max retries exceeded');
        }
        retry(fn, ms).then(resolve);
      }, ms);
    }
  })
});

function doFoo(opts) {
  // Return a Promise that resolves after doing something with opts
  // or rejects with err.statusCode
}

function doFooWithRetry(opts, ms = 1000, maxRetries = 2) {
  var attempt = function() {
    return doFoo(opts);
  }
  var retryable = function(err) {
    // example, retry on rate limit error
    if(err && err.statusCode == 429) {
      return true;
    } else {
      return false;
    }
  }

  return retry(attempt, ms, maxRetries, retryable);
}
Shyam Habarakada
  • 15,367
  • 3
  • 36
  • 47
0

The only easy-to-use and pure javascript zero dependency async-retry library you will ever need.

Example

const { retry } = require('@ajimae/retry')

function exec() {
  // This will be any async or sync action that needs to be retried.
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ message: 'some async data' })
    }, 1500)
  })
}

// takes the response from the exec function and check if the condition/conditions are met
function predicate(response, retryCount) {
  console.log(retryCount) // goes from 0 to maxRetries 

  // once this condition is met the retry exits
    return (response.message == 'some async data')
}

(async function main() {
  // enable or disable an exponential backoff behaviour if needed.
  const result = await retry(exec, predicate, { maxRetries: 5, backoff: true })
  console.log(result) // { message: 'some async data' } 
})()

PS: I authored this library.

ajimae
  • 89
  • 4