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

2

8

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.

Burt answered 3/9, 2013 at 21:50 Comment(5)
If your callbacks are non-promise callbacks, why would they interact with the promise at all?Royo
I... don't... know... ??? That's whay I'm trying to wrap it in when() so I can promisify just that method call.Burt
I thought it should work from the docs: "This is useful when you are dealing with an object that might or might not be a promise..." docs.angularjs.org/api/ng.$q#!Burt
does notAPromise call the callbacks asyncronously? If so you shouldn't need the $timeout. And even if you do need to use $timeout you should need to specify an amount of time (e.g. 1000). Other than that I think the way you've done it is the best way to do itGalanti
@Galanti You're right, $timeout is not needed and might be confusing the question.Burt
I
8

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

Ingram answered 13/5, 2014 at 8:35 Comment(7)
A good explanation and good ideas. I think it makes your answer "Don't do it that way at all" :)Burt
"It" is very hard to define here, while using "when" is not really applicable, your idea about having a better way to promisify that API was absolutely correct and if I understood the question correctly that was the actual issue :)Ingram
The question asks how to resolve() with a parameter. It's easy to reject() with a parameter using when(), but not resolve(), since resolve() is only a method of deferred, and can't be accessed with $q.resolve(...). The root of the question, though, is just as you say - a better way to promisify the API call. I was just trying to do it using $q.when().Burt
@Burt For the record, what $q.when implementation does is simply wrap the passed value with an extra deferred (smarter promise libraries can avoid even that when passed a promise) and then returns a promise on that (utilizing the fact promises recursively unwrap) - you can see it here github.com/angular/angular.js/blob/master/src/ng/q.js#L421-L467 , if you ignore the (rather useless) code for the extra parameters, it's basically function when(val){ var d = $q.defer(); d.resolve(val); return d.promise;}.Ingram
If it were exactly that, then the first bit of code would work since it would resolve with val right? In nextTick() instead of resolving with val it resolves with the result of ref(val).then(wrappedCallback, wrappedErrback, wrappedProgressback). Keep in mind I have enough knowledge to use a promise library, but designing one is tantalizingly out of reach. Thanks for your help!Burt
@Burt I've always found this an interesting and useful article to learn about the implementation of promises from modernjavascript.blogspot.com/2013/08/… , the Q library also contains snapshots of old builds that show the design train of thought that might be worth looking into.Ingram
While this doesn't answer the specific question, it will lead people in the right direction (probably away from $q.when()) and I hope solve their actual problem.Burt
R
0

Ok here's my interpretation of what I think you want.

I am assuming you want to integrate non-promise callbacks with a deferred/promise?

The following example uses the wrapCallback function to wrap two non-promise callbacks, successCallback and errCallback. The non-promise callbacks each return a value, and this value will be used to either resolve or reject the deferred.

I use a random number to determine if the deferred should be resolved or rejected, and it is resolved or rejected with the return value from the non-promise callbacks.

Non angular code:

function printArgs() {
    console.log.apply(console, arguments);
}

var printSuccess = printArgs.bind(null, "success");
var printFail = printArgs.bind(null, "fail");

function successCallback() {
    console.log("success", this);
    return "success-result";
}

function errCallback() {
    console.log("err", this);
    return "err-result";
}

function wrapCallback(dfd, type, callback, ctx) {
    return function () {
        var result = callback.apply(ctx || this, arguments);
        dfd[type](result);
    };
}

Angular code:

var myApp = angular.module('myApp', []);

function MyCtrl($scope, $q) {
    var dfd = $q.defer();
    var wrappedSuccess = wrapCallback(dfd, "resolve", successCallback);
    var wrappedErr = wrapCallback(dfd, "reject", errCallback);
    var rnd = Math.random();
    var success = (rnd > 0.5);
    success ? wrappedSuccess() : wrappedErr();
    console.log(rnd, "calling " + (success ? "success" : "err") + " callback");
    dfd.promise.then(printSuccess, printFail);
}

Example output where the random number is less than 0.5, and so the deferred was rejected.

err Window /fiddlegrimbo/m2sgu/18/show/
0.11447505658499701 calling err callback
fail err-result
Royo answered 3/9, 2013 at 23:29 Comment(2)
It looks like another way to handle the workaround from the question, but doesn't use when(). Create a deferred, and resolve/reject it explicitly. What I am trying to figure out is how/when to use when() to wrap non-promises.Burt
when - "Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. This is useful when you are dealing with an object that might or might not be a promise, or if the promise comes from a source that can't be trusted." i.e. it can turn something like a string into a promise - $.when("hello").then(function(response){ "hello" == response; });.Royo

© 2022 - 2024 — McMap. All rights reserved.