Comments:"What’s so great about JavaScript Promises? | Parse Blog"
URL:http://blog.parse.com/2013/01/29/whats-so-great-about-javascript-promises/
tl;dr– The Parse JavaScript SDK now returns jQuery-compatible Promises from most asynchronous methods. Read on to learn what that means.
“Promises” represent the next great paradigm in JavaScript programming. But understanding why they are so great is no simple matter. At its core, a Promise
represents the result of a task, which may or may not have completed. The only interface requirement of a Promise is having a function called then
, which can be given callbacks to be called when the promise is fulfilled or has failed. This is outlined in the CommonJS Promises/A proposal. For example, consider saving a Parse.Object
, which is an asynchronous operation. In the old callback paradigm, your code would look like this:
object.save({ key: value }, { success: function(object) { // the object was saved. }, error: function(object, error) { // saving the object failed. } });
In the new Promise paradigm, that same code would look like this:
object.save({ key: value }).then( function(object) { // the object was saved. }, function(error) { // saving the object failed. });
Not much different, right? So what’s the big deal? Well, the real power of promises comes from chaining multiple of them together. Calling promise.then(func)
returns a new promise, which is not fulfilled until func
has completed. But there’s one really special thing about the way func
is used. If a callback supplied to then
returns a new promise, then the promise returned by then
will not be fulfilled until the promise returned by the callback is fulfilled. The details of the behavior are explained in the Promises/A+ proposal. This is a complex topic, but maybe an example would make it clearer.
Imagine you’re writing code to log in, find an object, and then update it. In the old callback paradigm, you’d end up with what we call pyramid code:
Parse.User.logIn("user", "pass", { success: function(user) { query.find({ success: function(results) { results[0].save({ key: value }, { success: function(result) { // the object was saved. } }); } }); } });
That’s getting pretty ridiculous, and that’s without any error handling code even. But because of the way promise chaining works, the code can now be much flatter:
Parse.User.logIn("user", "pass").then(function(user) { return query.find(); }).then(function(results) { return results[0].save({ key: value }); }).then(function(result) { // the object was saved. });
Ah! Much better!
The code samples above left out error handling for simplicity, but adding it back reiterates what a mess the old callback code could be:
Parse.User.logIn("user", "pass", { success: function(user) { query.find({ success: function(results) { results[0].save({ key: value }, { success: function(result) { // the object was saved. }, error: function(result, error) { // An error occurred. } }); }, error: function(error) { // An error occurred. } }); }, error: function(user, error) { // An error occurred. } });
Because promises know whether they’ve been fulfilled or failed, they can propagate errors, not calling any callback until an error handler is encountered. For example, the code above could be written simply as:
Parse.User.logIn("user", "pass").then(function(user) { return query.find(); }).then(function(results) { return results[0].save({ key: value }); }).then(function(result) { // the object was saved. }, function(error) { // there was some error. });
Generally, developers consider a failing promise to be the asynchronous equivalent to throwing an exception. In fact, if a callback passed to then
throws an error, the promise returned will fail with that error. Propagating the error to the next available error handler is the asynchronous equivalent to bubbling up an exception until a catch
is encountered.
There are plenty of implementations of promises available to developers. For example, jQuery’s Deferred, Microsoft’s WinJS.Promise, when.js, q, and dojo.Deferred.
However, there is one interesting corner case to be aware of. As you can read in this long and fascinating jQuery pull request discussion, jQuery’s implementation does not quite fulfill the Promises/A spec in the way that most of the other implementations do. Experimentally, there’s only one case I’ve found where they actually diverge. If an error handler returns something other than a promise, most implementations consider the error handled, and don’t propagate it. However, jQuery does not consider the error handled in these cases, and propagates it forward anyway. While intermixing promises from different systems should normally work seamlessly, you should keep an eye out for this scenario. One potential workaround is to always return promises from your error handlers (instead of raw values), since they are always treated the same.
doFailingAsync().then(function() { // doFailingAsync doesn't succeed. }, function(error) { // Try to handle the error. return "It's all good."; }).then(function(result) { // Non-jQuery implementations will reach this with result === "It's all good.". }, function(error) { // jQuery will reach this with error === "It's all good.". });
In the latest release of Backbone 0.9.10, the asynchronous methods now return a jqXHR, which is a type of jQuery promise. One of the goals of the Parse JavaScript SDK is to maintain compatibility with Backbone as much as possible. We cannot return a jqXHR, because that wouldn’t work well in Cloud Code. So, we have added a class called Parse.Promise, which conforms to the jQuery Deferred semantics. In the latest version of the Parse JavaScript SDK, we have upgraded all of our asynchronous methods to return these new objects. The old callbacks are still accepted. But based on the examples listed above, we think you’ll prefer the new way. So give promises a try!