AngularJS Promise retries
Asked Answered
D

2

6

I am having some trouble getting a retry function to work and was hoping for some assistance. I have a $resource that I want to have called until a success condition occurs or the maximum retries has been exceeded.

The issue I seem to be running into is that within my retry function I am calling another promise and that is where the condition would be checked. I could get the code to function as intended by removing the added promise and creating a default success condition after a few retries but I cannot figure out how to correctly add the new promise call into the function.

resource is a stand-in for an Angular $resource which returns a $promise

My code is as follows:

resource.$promise.then(function (response) {
  return keepTrying(state, 5);
 }).then(function (response) {

 }).catch(function (err) {
   console.log(err);
 });

And the keepTrying function:

function keepTrying(state, maxRetries, deferred) {
  deferred = deferred || $q.defer();
  resource.$promise.then(function (response) {
    success = response;
  });
  if (success) {
    return deferred.resolve(success);
  } else if (maxRetries > 0) {
    setTimeout(function () {
      keepTrying(state, maxRetries - 1, deferred);
     }, 1000);
   } else if (maxRetries === 0) {
     deferred.reject('Maximum retries exceeded');
   }
   return deferred.promise;
 }
Daven answered 10/1, 2015 at 15:4 Comment(5)
That doesn't make any sense. Once resolved or rejected, the state of a promise will not change. Calling then just adds a new callback.Capitulum
My understanding is that by adding another then callback and returning a promise it will wait to resolve the then until the promise is resolved or rejected which I am trying to do within the keepTrying function.Daven
I'm not certain on what you're trying to do but look at the $q.defer().notifyTwentieth
I am attempting to run a new query within the retry function to check on the status of the original request via a different api endpoint.Daven
then doesn't change the underlying promise, it returns a new promise that will be resolved to the return value of your function.Litigation
A
11

The problem with your attempt is that you are not re-querying the resource, but using the promise for an already-queried resource over and over.

What you need to do is use a function that will (a) initiate the query, and (b) return the promise for that initiated query. Something like this:

function () { return $resource.get().$promise; }

Then you can pass it into something like this, that will do the retries.

function retryAction(action, numTries) {
    return $q.when()
        .then(action)
        .catch(function (error) {
            if (numTries <= 0) {
                throw error;
            }
            return retryAction(action, numTries - 1);
        });
}

Here's how you would start this off:

retryAction(function () { return $resource.get().$promise; }, 5)
.then(function (result) {
    // do something with result 
});

One nice thing about this approach is that even if the function that you pass to it throws an error upon invoking it, or doesn't return a promise at all, the retry functionality and the returning of the result via a resolved promise will still work.

Example

Aerosphere answered 10/1, 2015 at 17:6 Comment(7)
Wouldn't it be better returning $q.reject(error) instead of throwing an error?Yippee
@JesúsLópez throwing in this case is just fine and more idiomatic, but you may have a point in that throwing the original error is better than creating a new one and throwing that. That way the retry logic is transparent.Aerosphere
retryAction(function(){ rmaService.getRma.query({id: rmaId}).$promise }, 5).then( func... ) Trying to run this results the "then" block to be ran first. any way to solve this? rmaService returns $resourceKurd
@WillyPt You're missing the return before the rmaService... line. Please refer back to my example and see that there's a return there.Aerosphere
@Aerosphere yes, it's working now. I missed the return, that's why it ran first. Thanks!Kurd
it shouldn't be return $q.when(action()).catch(...) ?Yippee
@JesúsLópez No, the way you have it, if action() immediately threw an error, the .catch() portion would be unable to catch it and do the retry logic. I made a mention of this at the end of my answer.Aerosphere
K
1

You can implement a retryOperation function that returns a new promise upon error up to a maximum retry count thats passed as an argument:

function retryOperation(retryCount) {
    var count = 0;
    function success(result) {
        return result;
    }
    function error(result) {
         ++count;
         if (count <= retryCount)
             return $resource(...).query().$promise.then(success, error);

         throw 'max retries reached';
    }
    return $resource(...).query().$promise.then(success, error);
}

Usage

retryOperation(3).then(
    function success(result) {
        // done!
    }, 
    function error(reason) {
         alert(reason);
    });

var app = angular.module('app', []);
app.controller('ctrl', function($q) {
  function retryOperation(count) {
    var retryCount = 0;

    function operation() {
      var deferred = $q.defer();
      deferred.reject('failed');
      return deferred.promise;
    }

    function success(result) {
      return result;
    }

    function error(result) {
      ++retryCount;
      if (retryCount <= count) {
        alert('retrying ' + retryCount);
        return operation().then(success, error);
      }
      throw 'maximum retries reached';
    }
    return operation().then(success, error);
  }

  retryOperation(3).then(
    function success(result) {
      alert('done');
    },
    function error(result) {
      alert(result);
    });


});
<html>

<head>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

</head>

<body ng-app="app" ng-controller="ctrl">
  <input type="text" ng-model="name" />{{name}}
</body>

</html>
Karb answered 10/1, 2015 at 16:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.