125

I am trying to write a function that returns a promise. But there are times when the information requested is available immediately. I want to wrap it in a promise so that the consumer doesn't need to make a decision.

function getSomething(id) {
    if (Cache[id]) {
        var deferred = $q.defer();
        deferred.resolve(Cache[id]); // <-- Can I do this?
        return deferred.promise;
    } else {
        return $http.get('/someUrl', {id:id});
    }
}

And use it like this:

somethingService.getSomething(5).then(function(thing) {
    alert(thing);
});

The problem is that the callback does not execute for the pre-resolved promise. Is this a legitimate thing to do? Is there a better way to handle this situation?

Darren Shewry
  • 10,179
  • 4
  • 50
  • 46
Craig Celeste
  • 12,207
  • 10
  • 42
  • 49
  • 10
    A simpler way to write the return in the first case is `return $q.when(Cache[id])`. Anyhow, this should work and call the callback each time since you are creating new promises each time. – musically_ut Dec 12 '13 at 22:41
  • 3
    Working: http://plnkr.co/edit/OGO8T2M1fE3Mrgj2oozj?p=preview – JB Nizet Dec 12 '13 at 22:42
  • 1
    Crud. An hour of my life lost. I was trying this in a unit test and the promise is fulfilled after the test is complete, and I was not seeing it. Problem with my test and not the code. – Craig Celeste Dec 12 '13 at 22:50
  • Make sure you call $scope.$apply() to make sure things resolve right away during testing. – Daniel Tabuenca Dec 13 '13 at 08:39
  • I think httpbackend.flush accounts for this but $q might not. I'm not using scope in this test. I'm testing the service directly, but I got it working anyways, thanks. – Craig Celeste Dec 14 '13 at 12:09

5 Answers5

176

Short answer: Yes, you can resolve an AngularJS promise before you return it, and it will behave as you'd expect.

From JB Nizet's Plunkr but refactored to work within the context of what was originally asked (i.e. a function call to service) and actually on site.

Inside the service...

function getSomething(id) {
    // There will always be a promise so always declare it.
    var deferred = $q.defer();
    if (Cache[id]) {
        // Resolve the deferred $q object before returning the promise
        deferred.resolve(Cache[id]); 
        return deferred.promise;
    } 
    // else- not in cache 
    $http.get('/someUrl', {id:id}).success(function(data){
        // Store your data or what ever.... 
        // Then resolve
        deferred.resolve(data);               
    }).error(function(data, status, headers, config) {
        deferred.reject("Error: request returned status " + status); 
    });
    return deferred.promise;
    
}

Inside the controller....

somethingService.getSomething(5).then(    
    function(thing) {     // On success
        alert(thing);
    },
    function(message) {   // On failure
        alert(message);
    }
);
starball
  • 20,030
  • 7
  • 43
  • 238
Digital Fu
  • 2,877
  • 1
  • 14
  • 20
  • In case the http GET fails, the returned promise is not rejected this way. – lex82 Jun 11 '14 at 16:21
  • 6
    So the tl;dr for this post is: **Yes, you can resolve a promise before returning it, and it will short-circuit as intended.** – ray Sep 08 '14 at 15:43
  • 1
    This answer also applies to Kris Kowal's Q which Angular's promises are based on. – Keith Feb 24 '15 at 20:17
  • I added an error-handling example to your answer, I hope that's OK. – Simon East Nov 02 '15 at 05:04
  • Actually, this version is worse than the original one; there is no need to create yet another deferred/promise when he can use the $http, as done in the original answer. – Ricardo Nolde Jul 26 '16 at 02:31
98

How to simply return a pre-resolved promise in Angular 1.x

Resolved promise:

return $q.when( someValue );    // angular 1.2+
return $q.resolve( someValue ); // angular 1.4+, alias to `when` to match ES6

Rejected promise:

return $q.reject( someValue );
Andrey Mikhaylov - lolmaus
  • 23,107
  • 6
  • 84
  • 133
6

Here's how I typically do it if I want to actually cache data in array or object

app.factory('DataService', function($q, $http) {
  var cache = {};
  var service= {       
    getData: function(id, callback) {
      var deffered = $q.defer();
      if (cache[id]) {         
        deffered.resolve(cache[id])
      } else {            
        $http.get('data.json').then(function(res) {
          cache[id] = res.data;              
          deffered.resolve(cache[id])
        })
      }
      return deffered.promise.then(callback)
    }
  }

  return service

})

DEMO

charlietfl
  • 170,828
  • 13
  • 121
  • 150
0

You forgot to initialize the Cache element

function getSomething(id) {
    if (Cache[id]) {
        var deferred = $q.defer();
        deferred.resolve(Cache[id]); // <-- Can I do this?
        return deferred.promise;
    } else {
        Cache[id] = $http.get('/someUrl', {id:id});
        return Cache[id];
    }
}
zs2020
  • 53,766
  • 29
  • 154
  • 219
  • Sorry. That's true. I was trying to simplify the code for clarity in the question. Even so, if it goes into the pre-resolved promise, it doesn't seem to be calling the callback. – Craig Celeste Dec 12 '13 at 22:35
  • 2
    I don't think if you resolve a promise with a promise, the inner promise is flattened. This would populate the `Cache` with promises instead of intended objects and the return type for the cases when an object is in the Cache and when it isn't will not be the same. This is more correct, I think: `$http.get('/someUrl', {id: id}).then(function (response) { Cache[id] = response.data; return Cache[id]; });` – musically_ut Dec 12 '13 at 22:45
0

I like to use a factory to get the data from my resource something like.

.factory("SweetFactory", [ "$http", "$q", "$resource", function( $http, $q, $resource ) {
    return $resource("/sweet/app", {}, {
        "put": {
            method: "PUT",
            isArray: false
        },"get": {
            method: "GET",
            isArray: false
        }
    });
}]);

Then expose my model in the service like this here

 .service("SweetService",  [ "$q", "$filter",  "$log", "SweetFactory",
    function ($q, $filter, $log, SweetFactory) {

        var service = this;

        //Object that may be exposed by a controller if desired update using get and put methods provided
        service.stuff={
            //all kinds of stuff
        };

        service.listOfStuff = [
            {value:"", text:"Please Select"},
            {value:"stuff", text:"stuff"}];

        service.getStuff = function () {

            var deferred = $q.defer();

          var promise = SweetFactory.get().$promise.then(
                function (response) {
                    if (response.response.result.code !== "COOL_BABY") {
                        deferred.reject(response);
                    } else {
                        deferred.resolve(response);
                        console.log("stuff is got", service.alerts);
                        return deferred.promise;
                    }

                }
            ).catch(
                function (error) {
                    deferred.reject(error);
                    console.log("failed to get stuff");
                }
            );

            promise.then(function(response){
                //...do some stuff to sett your stuff maybe fancy it up
                service.stuff.formattedStuff = $filter('stuffFormatter')(service.stuff);

            });


            return service.stuff;
        };


        service.putStuff = function () {
            console.log("putting stuff eh", service.stuff);

            //maybe do stuff to your stuff

            AlertsFactory.put(service.stuff).$promise.then(function (response) {
                console.log("yep yep", response.response.code);
                service.getStuff();
            }).catch(function (errorData) {
                alert("Failed to update stuff" + errorData.response.code);
            });

        };

    }]);

Then my controllers can include it and expose it or do what it fells is right in its context simply by referencing the injected Service.whatever

Seems to work ok. But I am kinda new to angular. *error handling mostly left out for clarity

  • Your `getStuff` method is using the [deferred antipattern](https://stackoverflow.com/questions/23803743/what-is-the-deferred-antipattern-and-how-do-i-avoid-it) – Bergi Sep 12 '14 at 07:57