0

Recently started thinking that it was time to do a massive update to my logical operations, and part of that is the proper chaining of Angular JS asynchronous promise calls. Given the following code, how would I re-write it to be a proper chaining of two separate methods? (Yes, I've seen other posts about this, but they all deal with other versions of Angular, or other syntaxes, and I'm looking for something more up-to-date.)

vm.functionName = (
    function() {
        vm.processing = true;

        api.promise1({ Id: vm.Id })
            .then(
                function(result) {
                    if (result.error) {
                        notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(result.error));
                    } else {
                        api.promise2({ param: vm.param })
                            .then(
                                function(result2) {
                                    if (result2.error) {
                                        notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(result2.error));
                                    } else {
                                        vm.data = result2.data;
                                        notificationService.success("<h5>Operation successful!.</h5>");
                                    }

                                    vm.processing = false;
                                }
                            )
                            .catch(
                                function (err) {
                                    console.error(err);
                                    notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(err.statusText));
                                    vm.processing = false;
                                }
                            );
                    }
                }
            )
            .catch(
                function (err) {
                    console.error(err);
                    notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(err.statusText));
                    vm.processing = false;
                }
            );
    }
);

Logically, my brain tells me that I should be able to do something like this:

vm.functionName = (
    function() {
        vm.processing = true;

        vm.promise1()
          .then(
              vm.promise2()
                .then(
                    notificationService.success("<h5>Operation successful!.</h5>");
                    vm.processing = false;
                );
            );
          );
    }
);

vm.promise1 = (
    function() {
        api.promise1({ Id: vm.Id })
            .then(
                function(result) {
                    if (result.error) {
                        notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(result.error));
                    }
                }
            )
            .catch(
                function (err) {
                    console.error(err);
                    notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(err.statusText));
                }
            );
    }
);

vm.promise2 = (
    function() {
        api.promise2({ param: vm.param })
            .then(
                function(result) {
                    if (result.error) {
                        notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(result.error));
                    } else {
                        vm.data = result2.data;
                    }
                }
            )
            .catch(
                function (err) {
                    console.error(err);
                    notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(err.statusText));
                }
            );
    }
);

Update: the "api...." calls above call to my service.js layer, where methods exist like such:

promise1: function (params, error) {
    return $http
        .post("/C#Controller/Method1", params)
        .then(handleSuccess)
        .catch(function (e) {
            handleError(e, error);
        });
},

promise2: function (params, error) {
    return $http
        .post("/C#Controller/Method2", params)
        .then(handleSuccess)
        .catch(function (e) {
            handleError(e, error);
        });
},

Updated, per Pop-A-Stash's ideas, as now implemented:

//#region Api Calls and Helper
function apiCallOne() {
    return api.promise1({ Id: vm.Id });
}

function apiCallTwo() {
    return  api.promise2({param: vm.param });
}

function handleApiCallError(resultOrError, ngModelToSet) {
    var errMsg = resultOrError.statusText === undefined ? resultOrError.error === undefined ? "Unknown Error" : resultOrError.error : resultOrError.statusText;

    notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(errMsg));

    //This allows updating things like variables and ng-model goodies, via an inset function.
    if (ngModelToSet) {
        ngModelToSet();
    }
}
//#endregion

//#region Initialization
function init() {
    vm.pgLoaded = false;

    apiCallOne()
        .then(
            function(result) {
                if (!result.error) {
                    vm.data = result.data;
                    vm.pgLoaded = true;
                } else {
                    handleApiCallError(result, function() { vm.pgLoaded = true; });
                }
            }
        )
        .catch(function(errorOne) { handleApiCallError(errorOne, function() { vm.pgLoaded = true; }); });
}

init();
//#endregion
PKD
  • 685
  • 1
  • 13
  • 37
  • 1
    You forgot to `return` the promises from about every of your `function`s – Bergi Jun 20 '19 at 19:05
  • Using callbacks from promise `.then` [(or `.catch`) methods is an anti-pattern](https://stackoverflow.com/questions/35660881/why-are-callbacks-from-promise-then-methods-an-anti-pattern). – georgeawg Jun 20 '19 at 19:58
  • Nah - the return of them is in the service layer. And now, in the new methods that call them as well. – PKD Jun 20 '19 at 20:09
  • 1
    What is your "update per pop-a-stash"? Please add notes that explain it, rather than simply "per pop-a-stash" - If that's the _answer_, please don't include it in _the question_ - it creates confusion. Allow the question to stand as a question, and the various answer(s) to stand as answers. – random_user_name Jun 21 '19 at 14:45
  • @cale-b - The reason behind it was pretty simple. Pop-A-Stash gave an answer that steered me about 90% in the right direction. What I put in the update is expanded on his ideas, and the solution that I wound up with. So his answer is "how to", and my update is "how implemented". – PKD Aug 13 '19 at 17:37

2 Answers2

3

You could shorten your code significantly using recursion to call the next promise in an array of objects containing promises and their parameters using something similar to this:

function runPromises(promises) {
    var first = promises.shift();

    first.function(first.params).then(function(resp) {
        if (promises.length > 1) {
          runPromises(promises);
        } 
    }).catch(function (error) {
        handleError(error);
    });
}

with an initial array of something like this:

  var promises = [
      {
          function: promise1,
          params: any
      },
      {
          function: promise2,
          params: any
      }
    ];

If each promise response requires individual handling you could add a callback to be fired after the promise is resolved for each promise.

OliB
  • 104
  • 1
  • 5
1

If you want to chain them in a specific order, then you are already doing it correctly. However I see some code duplication that could be cleaned up:

vm.apiCallOne = apiCallOne;
vm.apiCallTwo = apiCallTwo;
vm.runChainedCalls = runChainedCalls;

function runChainedCalls() {
  vm.processing = true;

  vm.apiCallOne()
  .then(function(result1) {
    if(!result1.error) {
      vm.apiCallTwo().then(function(result2) {
        if(!result2.error) {
          notificationService.success("<h5>Operation successful!.</h5>");
          vm.data = result2.data;
          vm.processing = false;
         }
         else {
           handleError(result2);
         }
      })
      .catch(function(errorTwo) {
         handleError(errorTwo)
      });
    }
    else {
      handleError(result1);
    }
  })
  .catch(function(errorOne) {
    handleError(errorOne);
  });
}

function apiCallOne(){
  return api.callOne(param);  //don't forget to return the promise object
};

function apiCallTwo() {
  return api.callTwo(param);  //don't forget to return the promise object
};

function handleError(resultOrError) {
  notificationService.danger("<h5>An error occurred.</h5><h6>Details: {0}</h6>".format(resultOrError.statusText));
  vm.processing = false;
}

You only need one .then() and .catch() for each call in your controller. Anymore than that is code duplication.

If you want to run them concurrently and don't care about order, you would use the $q.all() function to run them at the same time:

function runConcurrentCalls() {

  $q.all([api.callOne(param), api.callTwo(param)]).then(function(responseArray) {
    // responseArray contains array of both call responses
    console.log(responseArray[0]);
    console.log(responseArray[1]);
  })
  .catch(function() {
    //something went wrong with one or both calls
  });
}
Pop-A-Stash
  • 6,572
  • 5
  • 28
  • 54
  • Order is vital, so I'll give your first code a try, and get back to you right away! Thanks for the speedy response! – PKD Jun 20 '19 at 17:09
  • Followed your example, but I wind up with `vm.apiCallOne is not a function` when I try to execute it. – PKD Jun 20 '19 at 17:56
  • You are calling the function before it is defined. If you follow conventional angularjs style guide you should be linking your controller functions to the vm at the top of your controller – Pop-A-Stash Jun 20 '19 at 18:15
  • And moving it up in the code... didn't fix the problem. – PKD Jun 20 '19 at 18:41
  • However, removing "vm.", and moving the function name to after "function" did. e.g. `function apiCallOne() { return api.promise1({ Id: vm.Id }); }` – PKD Jun 20 '19 at 18:43
  • reflected all of that in my update above. Thanks for the assistance! – PKD Jun 20 '19 at 18:48