Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resolving a deferred using Angular's $q.when() with a reason

I want to use $q.when() to wrap some non-promise callbacks. But, I can't figure out how to resolve the promise from within the callback. What do I do inside the anonymous function to force $q.when() to resolve with my reason?

promises = $q.when(
    notAPromise(
        // this resolves the promise, but does not pass the return value vvv
        function success(res) { return "Special reason"; },
        function failure(res) { return $q.reject('failure'); }
    ) 
);

promises.then(
    // I want success == "Special reason" from ^^^
    function(success){ console.log("Success: " + success); },
    function(failure){ console.log("I can reject easily enough"); }
);

The functionality I want to duplicate is this:

promises = function(){
    var deferred = $q.defer();

    notAPromise(
        function success(res) { deferred.resolve("Special reason"); },
        function failure(res) { deferred.reject('failure'); }
    ); 

    return deferred.promise;
};

promises.then(
    // success == "Special reason"
    function(success){ console.log("Success: " + success); },
    function(failure){ console.log("I can reject easily enough"); }
);

This is good, but when() looks so nice. I just can't pass the resolve message to then().


UPDATE

There are better, more robust ways to do this. $q throws exceptions synchronously, and as @Benjamin points out, the major promise libs are moving toward using full Promises in place of Deferreds.

That said, this question is looking for a way to do this using $q's when() function. Objectively superior techniques are of course welcome but don't answer this specific question.

like image 474
willoller Avatar asked Sep 03 '13 21:09

willoller


1 Answers

The core problem

You're basically trying to convert an existing callback API to promises. In Angular $q.when is used for promise aggregation, and for thenable assimilation (that is, working with another promise library). Fear not, as what you want is perfectly doable without the cruft of a manual deferred each time.

Deferred objects, and the promise constructor

Sadly, with Angular 1.x you're stuck with the outdated deferred interface, that not only like you said is ugly, it's also unsafe (it's risky and throws synchronously).

What you'd like is called the promise constructor, it's what all implementations (Bluebird, Q, When, RSVP, native promises, etc) are switching to since it's nicer and safer.

Here is how your method would look with native promises:

var promise = new Promise(function(resolve,reject){
    notAPromise(
        function success(res) { resolve("Special reason") },
        function failure(res) { reject(new Error('failure')); } // Always reject
    )                                                          // with errors!
);

You can replicate this functionality in $q of course:

function resolver(handler){
    try {
        var d = $q.defer();
        handler(function(v){ d.resolve(v); }, function(r){ d.reject(r); });
        return d.promise;
    } catch (e) {
        return $q.reject(e); 
        // $exceptionHandler call might be useful here, since it's a throw
    }
}

Which would let you do:

var promise = resolver(function(resolve,reject){
    notAPromise(function success(res){ resolve("Special reason"),
                function failure(res){ reject(new Error("failure")); })
});

promise.then(function(){

});

An automatic promisification helper

Of course, it's equally easy to write an automatic promisification method for your specific case. If you work with a lot of APIs with the callback convention fn(onSuccess, onError) you can do:

function promisify(fn){
    return function promisified(){
         var args = Array(arguments.length + 2);
         for(var i = 0; i < arguments.length; i++){
             args.push(arguments[i]);
        }
        var d = $q.defer();
        args.push(function(r){ d.resolve(r); });
        args.push(function(r){ d.reject(r); });
        try{
            fn.call(this, args); // call with the arguments
        } catch (e){  // promise returning functions must NEVER sync throw
            return $q.reject(e);
            // $exceptionHandler call might be useful here, since it's a throw
        }
        return d.promise; // return a promise on the API.
    };
}

This would let you do:

var aPromise = promisify(notAPromise);
var promise = aPromise.then(function(val){
    // access res here
    return "special reason";
}).catch(function(e){
    // access rejection value here
    return $q.reject(new Error("failure"));
});

Which is even neater

like image 85
Benjamin Gruenbaum Avatar answered Nov 15 '22 22:11

Benjamin Gruenbaum