Hi,

In this message, I'll be sharing some experience I've had with the Q library. I have worked with it for about 7-8 months in a medium/big Node.js application (closed source, so I can't link, sorry). I'll be covering only the parts that I have used during this experience. It's related to my own writing style and I don't mean that the rest is useless and should be thrown away, but the subset I'll be covering here has proven to be sufficient to my needs for several months.

I would be interested if others could share their experience if they had a different way of using promises.

# the Q API
## A Q Deferred is a {promise, reject, resolve} object. Only the deferred holder can resolve the promise (and not the promise holder), addressing the security issue that's been raised on the list.
You create a Deferred instance by calling Q.defer()

Typically, when trying to promise-ify async APIs, you end up doing something like (and that's probably also how things are done internally):

    function query(myQuery){
        var def = Q.defer();

        db.query(myQuery, function(err, data){
            if(err)
                def.reject(err)
            else
                def.resolve(data)
        })

        return def.promise;
    }

## A Q Promise is a {then, fail} object.
Given the previous code, it's possible to do:

    var someData1P = query(q1);
    someData1P.then(function(data){
        // do something with data
    });
    someData1P.fail(function(error){
        // handle the error
    });

Both then and fail return another promise for the result of the callback

    var someData1P = query(q1);
    var processedDataP = someData1P.then(function first(data){
return process(data); // if what's returned here is a promise P, then // processedDataP will be a promise for the resolution value of P, not
        // a promise for a promise of the resolution value of P
// Said another way, in a .then callback, the argument is always a non-promise value
    });

var processedAgain = processedDataP.then(function second(processedData){
        // play with processed data
    });

Of course, this second call returns a promise too, which I'm free to ignore.
If a database error occurred, neither the first nor the second callback will be called. Thanks to chaining, one interesting part is that I can hook a .fail only to th last promise in the chain to process the error. Following previous code:

    processedAgain.fail(function(err){
        // handle error, even if it's an error as "old" database error
    });

This is very close to how throw&try/catch work where you don't always need to handle the error as it happens, but you can catch it if no one else before you did. It's possible to forward an error by re-throwing in inside the callback. The above code could look like:

    var someData1P = query(q1);
    var processedDataP = someData1P.then(function first(data){
        return process(data);
    }).fail(function(err){
        // process err at this level and forward it.
        throw err;
    })

var processedAgain = processedDataP.then(function second(processedData){
        // play with processed data
    }).fail(function(err){
        // process the error
    });

In this case, "intermediate" promises are generated (before the .fail). I don't think this is too high of an overhead for the excellent readability it provides.

The fact that the .then of the next promise is called when the previous normally returned and the .fail when the previous threw makes me feel like promise chains have sort of two parallel channels to which one decides to branch to by returning or throwing.

I agree with what Kevin Smith said about the Promises/A+ aesthetic issue. The functions passed to .then and .fail are often function expressions (for me, the only exception was some final .fail callbacks for which an error handling function had been prepared in advance and was reused). I feel that when you have two function expressions separated only with a comma (to separate the onsuccess and onerror arguments), it's less easily readable than when you have your function expression prefixed with ".then(" or ".fail(". That's mostly writing style and aestetics so I won't be fighting to death to have both separated, but it feels like noticeable enough to be noted.


## Q.all
My favorite feature is the Q.all function. Q.all accepts an array of promises and returns a promise which will be fulfilled when all promises are. The resolution values are the different promises resolution values:

    var someData1P = query(q1);
    var someData2P = query(q2);
    var someData3P = fetch(url); // HTTP GET returning a promise

    Q.all([someData1P, someData2P, someData3P])
        .then(function(someData1, someData2, someData3){
            // do something when all data are back
        })

I used this extensively and it's been extremely helpful. Personally, to synchronize different async operations, I've never read code more elegant than what Q.all offers. I'm interested in hearing what other's experience is on that point. Arguably, Q.all could take several arguments instead of accepting only an array (that's one thing I'd change). Maybe there is a good reason to enforce an array, but I don't know it.

I think the .fail is called if any promise is broken you get only the first error as a result of Q.all, but you can always inspect each promise individually if you care about all errors. It never happened to me. Usually, when I did Q.all, I cared if all were successful or if one was broken, but several broken was not a use case I cared about. So, Q.all covered the 80% case (well, actually 100%) for me.



# Debugging
It's been said in other messages, one part where Q promises fell short was debugging. With thrown errors, if you uncatch one, your devtools/console will tell you. To my experience, with the Q library, if you forget a .fail, an error may end up being forgotten which is bad news in development mode (and I've wasted a lot of times not knowing an error had happened and chasing this errors after understanding that's why "nothing" was happening). I'm hopeful built-in promises will be able to compensate.

Basically, when a promise wasn't used to generate a new promise (to forward the error to), and ends up broken, you know you're facing an unhandled broken promise. The complicated (undecidable-style complicated) question is "how can one knows whether a promise will not be used anymore?" For sure, a GC'ed promise that hasn't been used to generate a new promise won't generate a new one, so if it's broken, it's clearly an unhandled broken. I've seen the promise.end and promise.aside in Mariusz Nowak post and both are very interesting. Specifically, .end is a way for developers to say "this promise chain is over" which devtools can easily interpret as "this promise won't forward the error any longer, I can report it as uncaught (if uncaught)". For built-in promises, there is no need to throw on failed promises lacking an onrejected I think (as it may be for a library implementing .end).

For un-GC'ed promises with no .end, I don't know what can be done. Maybe keep a record of all still-unhandled broken promises and have this accessible in the devtools? I don't know to which extent this is workable and useful. I'm confident it could be enough, but only user-research could really say.


# Promises and progress

Since I started talking about promises, I've had discussions with people and one thing that came about a couple of times was the idea of "progress" or how promises relate to streams.

The way I see promises, they have 3 states: unfulfilled/resolved/broken. I don't see an intermediate state. However, for some things (like data over the network or data pulled out of disk, or database records not coming all at once, etc.), one needs to consider that there are intermediate states. I think this is not a promise anymore, but rather a stream. As a developer, when needing to decide whether I'll use a promise or a stream, I just ask myself: "can I do anything useful with the partial result?" if yes, that's a stream, if no, that's a promise.


I think I've shared pretty much all my experience and thoughts on the topic. I feel that overall, the Q API is really good to work with promises and my opinion is that a standard promise feature should have the same features (I however don't care about the exact names of methods).

David
_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss

Reply via email to