AngularJS : $q -> deferred API order of things (lifecycle) AND who invokes digest?

Promises have three states

  • Pending – this is how promises start.
  • Fulfilled – this is what happens when you resolve a deferred, or when the return value from .then fulfills, and it generally analogous to a standard return value.
  • Rejected – This is what happens when you reject a deferred, when you throw from a .then handler or when you return a promise that unwraps to a rejection*, it is generally analogous to a standard exception thrown.

In Angular, promises resolve asynchronously and provide their guarantees by resolving via $rootScope.$evalAsync(callback); (taken from here).

Since it is run via $evalAsync we know that at least one digest cycle will happen after the promise resolves (normally), since it will schedule a new digest if one is not in progress.

This is also why for example when you want to unit test promise code in Angular, you need to run a digest loop (generally, on rootScope via $rootScope.digest()) since $evalAsync execution is part of the digest loop.

Ok, enough talk, show me the code:

Note: This shows the code paths from Angular 1.2, the code paths in Angular 1.x are all similar but in 1.3+ $q has been refactored to use prototypical inheritance so this answer is not accurate in code (but is in spirit) for those versions.

1) When $q is created it does this:

  this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) {
    return qFactory(function(callback) {
      $rootScope.$evalAsync(callback);
    }, $exceptionHandler);
  }];

Which in turn, does:

function qFactory(nextTick, exceptionHandler) {

And only resolves on nextTick passed as $evalAsync inside resolve and notify:

  resolve: function(val) {
    if (pending) {
      var callbacks = pending;
      pending = undefined;
      value = ref(val);

      if (callbacks.length) {
        nextTick(function() {
          var callback;
          for (var i = 0, ii = callbacks.length; i < ii; i++) {
            callback = callbacks[i];
            value.then(callback[0], callback[1], callback[2]);
          }
        });
      }
    }
  },

On the root scope, $evalAsync is defined as:

  $evalAsync: function(expr) {
    // if we are outside of an $digest loop and this is the first time we are scheduling async
    // task also schedule async auto-flush
    if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
      $browser.defer(function() {
        if ($rootScope.$$asyncQueue.length) {
          $rootScope.$digest();
        }
      });
    }

    this.$$asyncQueue.push({scope: this, expression: expr});
  },

  $$postDigest : function(fn) {
    this.$$postDigestQueue.push(fn);
  },

Which, as you can see indeed schedules a digest if we are not in one and no digest has previously been scheduled. Then it pushes the function to the $$asyncQueue.

In turn inside $digest (during a cycle, and before testing the watchers):

 asyncQueue = this.$$asyncQueue,
 ...
 while(asyncQueue.length) {
      try {
          asyncTask = asyncQueue.shift();
          asyncTask.scope.$eval(asyncTask.expression);
      } catch (e) {
          clearPhase();
          $exceptionHandler(e);
      }
      lastDirtyWatch = null;
 }

So, as we can see, it runs on the $$asyncQueue until it’s empty, executing the code in your promise.

So, as we can see, updating the scope is simply assigning to it, a digest will run if it’s not already running, and if it is, the code inside the promise, run on $evalAsync is called before the watchers are run. So a simple:

myPromise().then(function(result){
    $scope.someName = result;
});

Suffices, keeping it simple.

* note angular distinguishes throws from rejections – throws are logged by default and rejections have to be logged explicitly

Leave a Comment