Promises

David Bruant bruant.d at gmail.com
Fri Nov 9 04:33:50 PST 2012


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


More information about the es-discuss mailing list