[go: up one dir, main page]

Skip to content
Justin Vencel edited this page May 20, 2015 · 13 revisions

Welcome! Q works well alongside jQuery’s promise system, but the patterns are a little bit different.

In Q, promises are better suited to error handling, and maintaining the parallel between normal synchronous code and asynchronous promise code. jQuery's promises do not try to make this parallel, and thus jQuery and Q diverge in several design decisions which we outline here.

Exception Handling

jQuery's then does not handle exceptions thrown inside the handlers passed to then; instead, it lets them bubble, usually reaching window.onerror. Q, and Promises/A+ generally, instead maintains a strong correspondence: return means resolve the promise; throw means reject the promise. So code like this:

var promise2 = promise1.then(function () {
    throw new Error("boom!");
});

will result in promise2 being rejected in Q, whereas in jQuery, the thrown exception will be un-handleable by user code, instead bubbling to window.onerror.

The Promises/A+ model is much more powerful, as it maintains the parallel between synchronous code and asynchronous code. It allows you to reuse synchronous functions, like JSON.parse, that may throw, inside your asynchronous promise handlers. Those synchronous failures, when introduced into an asynchronous workflow, will create asynchronous failures, which you can then handle asynchronously inside the promise model. In this way, using Q gives you back control over your exceptions.

However, you do have to be careful with the Promises/A+ model. Since Q lets you handle your own exceptions, you need to not forget about them, letting them be trapped in rejected promises forever, with nobody to look at those rejections! The easiest way to do this is to follow the advice from the Q readme: always end a promise chain by either returning the promise to the caller, or calling .done() to signify that any unhandled rejections are no longer your responsibility, and should be thrown.

Converting JQuery Promises to Q

Converting from jQuery promises to Q is easy! Simply pass the promise into the Q function:

return Q(jQuery.ajax({
    url: "foobar.html", 
    type: "GET"
})).then(function (data) {
    // on success
}, function (xhr) {
    // on failure
});

// Similar to jQuery's "complete" callback: return "xhr" regardless of success or failure
return Q.promise(function (resolve, reject) {
    jQuery.ajax({
        url: "foobar.html",
        type: "GET"
    }).then(function (data, textStatus, jqXHR) {
        delete jqXHR.then; // treat xhr as a non-promise
        resolve(jqXHR);
    }, function (jqXHR, textStatus, errorThrown) {
        delete jqXHR.then; // treat xhr as a non-promise
        reject(jqXHR);
    });
});

Chaining

jQuery's done and fail methods are consistent with jQuery's internal ideas about chaining, but inconsistent with common usage of promises. In jQuery, chaining functions usually return the this object of the call so you can build up lots of handlers on a single object. With promises, though, it is far more common that you will only need to set up handlers on the promise once, modeling the linear, non-branching synchronous control flow of return, try, catch, and finally—but asynchronously.

All of Q’s methods on promises return a new promise, not this. The new promise gets resolved with the return value of either the fulfillment handler or the rejection handler. Only one of these functions will be called, and each function can only return a value or throw an exception. If a handler throws an exception, the promise gets rejected. If a handler returns a promise, it gets “forwarded” to the promise that was originally returned.

The practical upside of this is that in jQuery, the methods done and fail are often used to attach handlers by taking advantage of their this-returning nature. Thus, where in Q you might write

qPromise.then(success, failure);

with jQuery promises people often write

jQueryPromise.done(success).fail(failure);

In Q, the first pattern is correct. fail is an alias for catch, which, just like then, will return a new promise. And done is something else entirely. It is essentially a version of then that signifies the end of the chain. It will return undefined, and any errors thrown in its handlers will be re-thrown, since there is no promise returned for it to forward them to.

With Q, you can attach multiple handlers to a single promise, but you have to reuse a reference to the promise instead of chaining.

var promiseB = qPromiseA.then(makeB);
var promiseC = qPromiseA.then(makeC);
qPromiseA.catch(handleFailureA).done();

This creates a fork in your control flow, where the fulfillment of qPromiseA will kick off two parallel and independent jobs to resolve promises B and C using the return values of makeB and makeC, but the rejection of qPromiseA will cause an attempt to recover with handleFailureA, and if that fails, throw an exception to the user.

Asynchrony

jQuery's promises are not guaranteed to be asynchronous. Thus, code like

jQueryPromise.then(function () {
    myObject.state = "X";
});

myObject.state = "A";

could either end up with your object in state "X", or in state "A", depending on whether jQueryPromise was an asynchronous promise or a synchronous promise. This introduces control-flow hazards and race conditions into your code, as you cannot expect any given execution order for jQuery promise-based code.

In Q, and indeed in all Promises/A+ compliant libraries, promises are guaranteed to be asynchronous. So code of the form

qPromise.then(function () {
    myObject.state = "X";
});

myObject.state = "A";

will always end up with your object in state "X".

Single vs. Multiple Values

In Q, all promises can have either a single value, if fulfilled, or a single reason, if rejected. This parallels how a synchronous function can either return a single value, or throw a single exception.

This is different from jQuery, which chooses to forgo the sync/async parallel, and instead commonly pass multiple values to its promise handlers. So if you are used to writing code like

$.ajax(...).then(function (data, status, xhr) {
    console.log(data, status, xhr);
});

when transitioning to Q you should treat promise-returning functions like normal synchronous functions, in that you should assume they only return a single object, perhaps like so:

qAjax(...).then(function (result) {
    console.log(result.data, result.status, result.xhr);
});

If you are using the "multiple return values" feature for something that should actually be treated as an array, then you can just use an array. Q even provides the spread function to make this a bit easier:

$.when(jQueryPromise1, jQueryPromise2).then(function (result1, result2) {
    
});

// becomes

Q.all([qPromise1, qPromise2]).spread(function (result1, result2) {
    
});

Deferreds, Promises, Resolvers

jQuery balances the principle of least-authority differently than Q. Q separates authority by default and jQuery provides a mechanism for separating authority.

Both Q and jQuery provide a deferred object that hosts the authorities of observing state and changing state. In Q, the deferred has a promise property that is the sole interface for observing state. The resolve and reject functions change state. Only the promise has the promise API in Q. In jQuery, the $.Deferred object is both the promise and the deferred, but it provides a promise() function that can either return the promise part of the API, or put the promise methods on another object.

function foo() {
    var result = Q.defer();
    result.resolve(10);
    return result.promise;
}
function foo() {
    var result = $.Deferred();
    result.resolve(10);
    return result; // or
    return result.promise();
}

Q is less prone to accidental gifting of excess authority. It’s easier to give than to take back.

Context

Q does not track the context object that goes with a fulfilled value since this cannot be expressed with a return value or thrown exception in the synchronous parallel. As such, if you want a particular this bound in your handler functions, you will need to bind it yourself or bind it to another value in scope. It is not the resolving party's job to decide what this the handler should be called with.

var self = this;
promise.then(function () {
    // use self
});
promise.then(function () {
    // use this
}.bind(this));

Terminology

Q uses the Promises/A+ terminology, wherein promises can be in one of three states:

  • Pending, wherein the promise does not yet have a value;
  • Fulfilled, wherein the promise has a value;
  • Rejected, wherein the promise does not have a value but does have a reason why it couldn't give you that value.

Additionally, Promises/A+ uses the word "settled" to mean "either fulfilled or rejected."

jQuery promises have states that are somewhat analogous to these. The biggest difference is that they use "resolved" where Promises/A+ says "fulfilled." In Q, "resolved" actually means "has been locked in to one course of action"; a promise can be in any of the three states, but also be resolved. For more details on this see "States and Fates".

Reference

jQuery Q Notes
then then Q's then, and in fact all of its methods, have different exception-handling behavior, as described above.
done then then does not support multiple handlers; use multiple calls to then to attach them.
fail catch catch does not support multiple handlers; use multiple calls to catch to attach them.
progress progress progress does not support multiple handlers; use multiple calls to progress to attach them.
always finally finally has semantics that better parallel synchronous try-finally, instead of simply passing the same handler for both fulfillment and rejection.
isResolved isFulfilled As noted above, jQuery uses the term "resolved" where Promises/A+ libraries use "fulfilled."
isRejected isRejected
resolve deferred.resolve If you pass a promise to Q's resolve, then deferred.promise will follow the passed promise's state.
resolveWith (none) Q does not allow the deferred to control the this of the promise handlers.
reject deferred.reject
rejectWith (none) Q does not allow the deferred to control the this of the promise handlers.
notify deferred.notify
notifyWith (none) Q does not allow the deferred to control the this of the promise handlers.
deferred.promise (method) deferred.promise (property) You *must* get the promise part of the deferred; the deferred does not have the promise API.
state inspect inspect returns state snapshot objects in the form { state, value|reason }.
$.when Q or Q.all Use the Q function to turn the non-promise values into promises, or pass an array to Q.all to get back a promise for an array of the results.