5

I have a question regarding Javascript promise chaining. Say I have an action somewhere down a chain of promise. That action does not return any value but it must complete before the chain can proceed.

Do I need to wrap that action in a promise? Do I need something like:

$q.when();

See my code below:

...
var goToDashboard = function () {
    //TODO: use $q here?
    $state.go('dashboard');
};
...
activateEmail().then(signinByToken).then(setPersonalInfo).then(goToDashboard).then(somethingElse).catch(reportProblem);

Can someone please advise?

balteo
  • 23,602
  • 63
  • 219
  • 412

2 Answers2

3

In the below I've demonstrated chaining .then's using promises from various functions, and other return types. Without returning a promise, of course, there is no delayed resolution and the following .then executes immediately - so if you have something async that needs to complete, you need to return a promise that resolves when the async task is completed. Note that returning $q.when() does return a promise (wrapped around anything you provide as a param), but it's immediately resolved.

Also, note that $state.go actually returns a promise! So in your above example, you can just return $state.go('dashboard'); and the following .then shouldn't execute until ui-router has changed the route (demonstrated below).

(function() {
  "use strict";

  angular.module('myApp', ['ui.router', 'ngResource'])
    .controller('myController', ['$scope', '$state', '$q', '$timeout', '$resource', '$log', MyController])
    .config(['$stateProvider', configUiRouter]);

  function configUiRouter($stateProvider) {
    $stateProvider
      .state("home", {
        url: "/home",
        template: "<div>Home state</div>"
      })
      .state("dashboard", {
        url: "/dashboard",
        template: "<div>Dashboard state</div>"
      })
      .state("error", {
        url: "/error",
        template: "<div>Error state: I'm sorry Dave, I'm afraid I can't do that...</div>"
      });
  }

  function MyController($scope, $state, $q, $timeout, $resource, $log) {

    $scope.status = {
      emailActivated: false,
      signinByToken: false,
      personalInfo: false,
      stackoverflowUsers: null,
      loading: null,
      counter: 0
    };

    $state.go('home'); // set default state for ui-router test

    activateEmail()
      .then(updateStatusLoading).then(counting) // Loading: . Counter: 1
      .then(signinByToken)
      .then(updateStatusLoading).then(counting) // Loading: .. Counter: 2
      .then(setPersonalInfo)
      .then(updateStatusLoading).then(counting) // Loading: ... Counter: 3
      .then(goToDashboard)
      .then(updateStatusLoading).then(counting) // Loading: .... Counter: 4
      .then(somethingElse)
      .then(triggerError)
      .then(neverReached)
      .catch(catchesReject);


    /* * * * * * * * * * *
     * Promise functions *
     * * * * * * * * * * */

    // doesn't return any promise
    // (resolves immediately)
    function updateStatusLoading() {
      if (!$scope.status.loading) {
        $scope.status.loading = "";
      }
      $scope.status.loading += ".";
    }

    // returns something other than a promise (a String...)
    // (resolves immediately)
    function counting() {
      $scope.status.counter++;
      return "Did some counting... (" + $scope.status.counter + ")";
    }

    // using promise returned by $timeout
    // (delayed resolution)
    function activateEmail() {
      return $timeout(function simulateActivateEmailLatency() {
        $scope.status.emailActivated = true;
      }, 1000);
    }

    // using promise returned by $q.defer, resolved in a $timeout
    // (the return is immediate, but the resolve is delayed)
    function signinByToken() {
      var deferred = $q.defer();

      $timeout(function simulateSignInLatency() {
        $scope.status.signinByToken = true;
        deferred.resolve({
          returningSomething: "Is entirely optional"
        });
      }, 1000);

      //log to console what this object looks like
      $log.log("deferred.promise: ", deferred.promise);

      return deferred.promise;
    }

    // using promise created by $q.when; no timeout
    // (immediate resolution)
    function setPersonalInfo() {
      $scope.status.personalInfo = true;

      $log.log("$q.when: ", $q.when({
        foo: "bar"
      }));

      return $q.when({
        returningSomething: "Is entirely optional"
      });
    }

    // using promise created by $state.go
    // (will resolve once route has changed; which could include time spent doing ui-router resolves...)
    function goToDashboard() {
      // yup, this returns a promise!
      // https://github.com/angular-ui/ui-router/wiki/Quick-Reference#stategoto--toparams--options
      var goPromise = $state.go('dashboard');

      $log.log("$state.go: ", goPromise);

      return goPromise;
    }

    // using $promise returned by resource, and adding an .then
    // (resolves when the $resource does)
    function somethingElse() {
      var resourceContainingPromise = $resource('https://api.stackexchange.com/2.2/info')
        .get({
          site: 'stackoverflow'
        });

      // (note that it contains a $promise, is not a promise itself)
      $log.log("$resource: ", resourceContainingPromise);

      return resourceContainingPromise
        .$promise
        .then(function resourceHandler(results) {
          $scope.status.stackoverflowUsers = results.items[0].total_users;
        });
    }

    // returns a rejected promise
    // (immediate resolve)
    function triggerError() {
      var rejectPromise = $q.reject("An error message");

      $log.log("$q.reject: ", rejectPromise);

      return rejectPromise;
    }

    // this gets skipped due to .triggerError()
    function neverReached() {

      $log.error("Shouldn't see this!");

      $scope.status.loading += "Never reached!";
    }

    // this catches the $q.reject and logs the data it passed...
    function catchesReject(data) {
      $log.log(data); //log the error message
      return $state.go('error');
    }

  }
})();
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular-resource.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.14/angular-ui-router.min.js"></script>
<div ng-app="myApp">
  <div ng-controller="myController">
    <div style="display:inline-block; float:left; margin-right: 20px; min-width: 250px;">
      <ul>
        <li>Email activated: {{status.emailActivated}}</li>
        <li>Sign-in by Token: {{status.signinByToken}}</li>
        <li>Personal info set: {{status.personalInfo}}</li>
        <li>Stackoverflow Users: {{status.stackoverflowUsers}}</li>
      </ul>
      <hr />
      Loading: {{status.loading}}
      <br />Counter: {{status.counter}}
    </div>
    <div style="display:inline-block; padding: 10px; border: 1px solid grey; max-width: 150px;">
      <strong>Ui Router Test</strong>
      <div ui-view></div>
    </div>
  </div>
</div>
JcT
  • 3,539
  • 1
  • 24
  • 34
0

I think I have found the answer to my question.

First, one needs to take into account the fact that then(), to quote the documentation, returns a new promise which is resolved with the return value of the callback.

See below:

then(successCallback, errorCallback, notifyCallback) – regardless of when the promise was or will be resolved or rejected, then calls one of the success or error callbacks asynchronously as soon as the result is available. The callbacks are called with a single argument: the result or rejection reason. Additionally, the notify callback may be called zero or more times to provide a progress indication, before the promise is resolved or rejected.

This method returns a new promise which is resolved or rejected via the return value of the successCallback, errorCallback (unless that value is a promise, in which case it is resolved with the value which is resolved in that promise using promise chaining). It also notifies via the return value of the notifyCallback method. The promise cannot be resolved or rejected from the notifyCallback method.

So I assume that the following callback (that does return something explicitly) will just return undefined itself wrapped into a promise by then():

var goToDashboard = function () {
    //TODO: use $q here?
    $state.go('dashboard');
};

So I do have a promise - thanks to then() - and I don't need anything else...

balteo
  • 23,602
  • 63
  • 219
  • 412
  • I believe you may need to return the promise returned by $state.go in order for your chain to delay its continued execution until the state change completes, since it's an async task. http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues – JcT May 15 '15 at 15:34