defer.reject() not caught within async promise

1,331 views
Skip to first unread message

ni...@silverbucket.net

unread,
Mar 21, 2013, 9:53:19 PM3/21/13
to ang...@googlegroups.com
Hi all,

 I'm trying out AngularJS in combination with remoteStorage.js (http://remotestorage.io). As the app initializes, I have the router call the appCtrl.initializeData() function, which tries to connect to remoteStorage and get some necessary configuration data. When remoteStorage is not connected, I throw a defer.reject, which is caught by a $routeChangeError listener.

However, when remoteStorage is connected, then do an async fetching of the config, if the config is not found, i use the defer.reject and nothing happens. AngularJS never seems to react to it at all. I've posted this to the remoteStorage.js issues as well as I'm not sure if it's an Angular or remoteStorage issue (or user error).

Here's the basic setup:
var appCtrl = dogtalk.controller('appCtrl', function ($scope, $rootScope, $route, $location) {

  $rootScope.$on("$routeChangeStart", function (event, current, previous, rejection) {
    console.log('routeChangeStart: ', $scope, $rootScope, $route, $location);
  });

  $rootScope.$on("$routeChangeSuccess", function (event, current, previous, rejection) {
    console.log('routeChangeSuccess: ', $scope, $rootScope, $route, $location);
  });

  $rootScope.$on("$routeChangeError", function (event, current, previous, rejection) {
    console.log('routeChangeError: ', rejection);
  });

});


appCtrl.initializeApp = function ($q, $timeout) {
  var defer = $q.defer();
  $timeout(function () {
    if (remoteStorage.getBearerToken() === null) {
      defer.reject("remoteStorage not connected");
    } else {
      remoteStorage.sockethub.getConfig().then(function (config) {
        console.log('got config: ', config);
        return sockethubConnect(config);
      }).then(function () {
          defer.resolve();
      }, function (error) {
        defer.reject("config not found");
      });
    }
  }, 0);
  return defer.promise;
};

In the case above -- if that first defer is reached (remoteStorage not connected) the routeChangeError is triggered as expected. However, if the second reject is reached (config not found), routeChangeError is never triggered.

Don't know what other kind of debugging or information I can provide, any help is greatly appreciated.
Here is the issue posted to remoteStorage github page, there's a bit more of a breakdown there:

https://github.com/remotestorage/remotestorage.js/issues/291

Cheers
Nick


ni...@silverbucket.net

unread,
Mar 21, 2013, 9:54:47 PM3/21/13
to ang...@googlegroups.com
Oh, and the actual code lives here:
https://github.com/sockethub/dogtalk

Andy Joslin

unread,
Mar 21, 2013, 11:12:43 PM3/21/13
to ang...@googlegroups.com
$q promises will run their resolve and reject on the next angular digest loop.  Is remoteStorage.socketHub.getConfig() running a digest with scope.$apply?  If not, add that in there.  same for sockethubConnect function.  Make sure those run a digest after they're done, or their deferreds will wait for a digest until going forward.

Also a minor thing: you could also use $timeout as the promise you return, and make your code cleaner.

Here's the code with the promise improvements. Much shorter and awesomer :-).  You can generally just keep returning promises forever.

appCtrl.initializeApp = function ($q, $timeout) {
  return $timeout(function () {
    if (remoteStorage.getBearerToken() === null) {
      return $q.reject("remoteStorage not connected");
    }
    //Make sure once getConfig is done it runs a $digest 
    return remoteStorage.sockethub.getConfig().then(function (config) {

      console.log('got config: ', config);
      //Make sure once sockethubConnect is done it runs a $digest
      return sockethubConnect(config);
    });
  }, 0);
};

Nick Jennings

unread,
Mar 30, 2013, 4:55:06 AM3/30/13
to ang...@googlegroups.com
Hi Andy, thanks for your reply, and sorry for the delayed response.
I've been travelling and had no time to give this a real shot until
now.


On Fri, Mar 22, 2013 at 4:12 AM, Andy Joslin <andyt...@gmail.com> wrote:
> $q promises will run their resolve and reject on the next angular digest
> loop. Is remoteStorage.socketHub.getConfig() running a digest with
> scope.$apply? If not, add that in there. same for sockethubConnect
> function. Make sure those run a digest after they're done, or their
> deferreds will wait for a digest until going forward.

I'm not sure I understand, but the remoteStorage and Sockethub
libraries don't know anything about AngularJS, so they are not doing
anything special for AngularJS. Do you have some pointers on where I
could read up on what you're talking about?

I'm very new to AngularJS so, I don't understand a lot of what's going
on behind the scenes. I just get this silently failing behavior and
have no idea where to start digging.

I tried your example, but the behavior is still the same. Obviously I
don't fully understand what's going on here, so any pointers to things
I should read about to help me solve this would be very helpful.
Thanks
Nick


> Also a minor thing: you could also use $timeout as the promise you return,
> and make your code cleaner.
>
> Here's the code with the promise improvements. Much shorter and awesomer
> :-). You can generally just keep returning promises forever.
>
> appCtrl.initializeApp = function ($q, $timeout) {
> return $timeout(function () {
> if (remoteStorage.getBearerToken() === null) {
> return $q.reject("remoteStorage not connected");
> }
> //Make sure once getConfig is done it runs a $digest
> return remoteStorage.sockethub.getConfig().then(function (config) {
>
> console.log('got config: ', config);
> //Make sure once sockethubConnect is done it runs a $digest
> return sockethubConnect(config);
> });
> }, 0);
> };
>
> --
> You received this message because you are subscribed to a topic in the
> Google Groups "AngularJS" group.
> To unsubscribe from this topic, visit
> https://groups.google.com/d/topic/angular/zyhMQRuNqM0/unsubscribe?hl=en-US.
> To unsubscribe from this group and all its topics, send an email to
> angular+u...@googlegroups.com.
> To post to this group, send email to ang...@googlegroups.com.
> Visit this group at http://groups.google.com/group/angular?hl=en-US.
> For more options, visit https://groups.google.com/groups/opt_out.
>
>

Nick Jennings

unread,
Mar 30, 2013, 5:21:11 AM3/30/13
to ang...@googlegroups.com
On Fri, Mar 22, 2013 at 4:12 AM, Andy Joslin <andyt...@gmail.com> wrote:
> Is remoteStorage.socketHub.getConfig() running a digest with
> scope.$apply? If not, add that in there.

Also, needless to say, scope.$apply is not defined within the
getConfig() function. I'm confused as to how to interact with
AngularJS at all from within external JavaScript (JavaScript that is
not within an Angular controller or module scope)

Nick Jennings

unread,
Mar 31, 2013, 5:42:17 AM3/31/13
to ang...@googlegroups.com
Does anyone have any further insight on this? It's still really confusing to me that an async call to an external library is somehow breaking Angular. This has completely stopped progress, and I'm starting to question my reasons for choosing to develop with Angular (I thought I'd be able to get up and runing relatively fast), perhaps it's still got some bugs? Or should all Angular development be done without the use of "external" JavaScript (which makes it a show stopper for me)?

Thanks for any help
Nick


Nick Jennings

unread,
Mar 31, 2013, 5:46:45 AM3/31/13
to ang...@googlegroups.com
Just to illustrate the problem. Here's a stripped down example of where Angular breaks when you don't use "Angular-specific" functions:

This is working:

appCtrl.initializeApp = function ($q, $timeout) {
  var defer = $q.defer();
  $timeout(function() {
    defer.reject('something');
  }, 0);
  return defer.promise;
};

This is not working:

appCtrl.initializeApp = function ($q, $timeout) {
  var defer = $q.defer();
  setTimeout(function() {
    defer.reject('something');
  }, 0);
  return defer.promise;
};
 
The only difference here is, whether the $timeout wrapper is used or not.

Josh Kurz

unread,
Mar 31, 2013, 8:48:25 AM3/31/13
to ang...@googlegroups.com
Well see timeout calls $apply so that's really the difference. You second function is calling a digest loop which in turn is not updating your scope.

Sent from my iPhone
--
You received this message because you are subscribed to the Google Groups "AngularJS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to angular+u...@googlegroups.com.

Nick Jennings

unread,
Mar 31, 2013, 9:54:22 AM3/31/13
to ang...@googlegroups.com
Do you have an example of how the second function could be fixed to work with Angular?



--
You received this message because you are subscribed to a topic in the Google Groups "AngularJS" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/angular/zyhMQRuNqM0/unsubscribe?hl=en-US.
To unsubscribe from this group and all its topics, send an email to angular+u...@googlegroups.com.

Nick Jennings

unread,
Mar 31, 2013, 10:38:03 AM3/31/13
to ang...@googlegroups.com
Based on your response I found some others who'd run into this before. One solution was to do something like this:

appCtrl.initializeApp = function ($q, $timeout, $rootScope) {

  var defer = $q.defer();
  setTimeout(function() {
    console.log('fail');
    $rootScope.$apply(function () {
      defer.reject('fail');

    });
  }, 0);
  return defer.promise;
};


Is this the way it's supposed to be done in Angular? Everytime a non-Angular async call is made, there must be a $rootScope.$apply call made? I don't really understand why, perhaps there's a keyword I'm missing here as to what this concept is called, because I can't find any real information on it, and right now am just mimicking what I'm seeing elsewhere without really understanding what's going on here.

Any insight would be greatly appreciated.
Cheers
Nick

Nick Jennings

unread,
Apr 2, 2013, 3:42:13 AM4/2/13
to ang...@googlegroups.com
Also, as a follow-up here. I've seen warnings against always injecting "$rootScope"... though I don't know enough about it to make my own choice. In this case it seems like the only way to go. So for any functions I will need to use a non-angular async function, I will need to inject $rootScope?

Cheers
Nick

Kai Groner

unread,
Apr 2, 2013, 10:59:53 AM4/2/13
to ang...@googlegroups.com
Hi Nick,

You've figured out what's going on, so I'll try to explain why.

Anytime we make a change in a non-angular event handler, we need to let Angular know it should run a $digest().  This shouldn't be a huge burden and it shouldn't discourage you from integrating with other libraries.  Even working with native events and timers requires this.  Generally directives and services will hide these details, so your application logic doesn't need to be aware of it.

Promises introduce another wrinkle, because $q integrates with the $digest loop but resolving or rejecting a deferred doesn't schedule a digest.  So resolving or rejecting a deferred doesn't have an immediate effect when invoked from a non-angular event.

  • $q is integrated with the ng.$rootScope.Scope Scope model observation mechanism in angular, which means faster propagation of resolution or rejection into your models and avoiding unnecessary browser repaints, which would result in flickering UI.

I think the reason for not injecting $rootScope, is to discourage people from using it to stuff global variables into when they should write a separate controller and maybe a service.  If you're injecting $rootScope because you need to run a $digest(), I don't think that's a bad thing.  When I'm writing a service that doesn't otherwise work with a scope I'll use $timeout() for this.


Kai

Nick Jennings

unread,
Apr 2, 2013, 11:46:31 AM4/2/13
to ang...@googlegroups.com
Hi Kai, thank you very much for the explanation. It's all starting to make a lot more sense. :)

Reply all
Reply to author
Forward
0 new messages