(This post updated to talk about Promises, the final accepted form of the API.)
Promises were only recently introduced, and it seems like suddenly every API is being forced into using them. Why? How did this crazy idea suddenly appear? Where was the discussion? Hopefully I can answer a few of these questions, and explain why Promises are so useful and should be used widely.
Promise History
First of all, Promises didn't spring fully-formed from anyone's head. If you've been doing JS programming the last several years with any of the popular frameworks, you've probably already used Promises, though they might have been called "Futures", "Deferreds", or "Thenables".
Specifically, Promises are based on the Promises/A+ version, standardized by Domenic Denicola and others. Over the last several years, this has turned out to be the most popular and most technically worthy version of Promises.
About two years ago, Alex Russell gathered together several influential and super-smart people with experience in modern webdev, like Erik Arvidsson, Yehuda Katz, Jake Archibald, and others, and started iterating on a proposal to do Promises on the web. While doing so, he also kept some awesome old-hand programming language designers, like Mark Miller, in the loop so that obvious pitfalls and historical errors could be avoided. The final API ended up being Promises/A+ compatible, and had a very sharp, small API that still represented tons of power. This was then ported over into the DOM spec by Anne van Kesteren as DOM Futures, and then made its way into the proper ECMAScript spec as Promises.
In short, this isn't some half-baked idea put together in five minutes. It's based on years of real-world experience and lots of thought from really smart hackers.
Promise Value
What exactly do Promises bring to the table? Why are they better than Events, or just callbacks?
There are lots of reasons to use some form of callback-based asynchronous API, but one popular reason is because you have some task that you don't want to block the main thread on (maybe it requires file or network IO, for example), and you want to be able to return its value when it finishes.
This sort of pattern shows up everywhere in DOM and related APIs. Unfortunately, it's implemented in a myriad of ways. Some APIs return a dummy object, and then fire a DOM Event at it when the operation is finished. Others fire a DOM Event at an appropriate global interface, with some information to figure out what thing the event was for. Others return a dummy object with a few callback-registering functions (similar to, but not using, DOM Events). Others take callbacks directly in the argument list. Others take callbacks in an options object.
And then, with all the above methods, APIs may or may not have some way to detect errors in the operation, which increases the complexity further. APIs may or may not have an easy way for multiple functions to be registered for the event. APIs may or may not have a way for code to get at the value of the operation after it completes.
All of this adds up to a metric fuckton of API surface for something that is a very simple, small set of meaningful API concepts.
The core value of Promises is that it unifies all of these into a single, idiomatic pattern. Whenever a function will kick off an async task, it should return a Promise. The user can then register "accept" or "reject" callbacks on the Promise, which get called when the operation completes successfully or fails with an error, respectively. You can register callbacks multiple times, and they'll all be called as appropriate. After the operation completes, you can still register callbacks on the future, and it'll just call them "immediately" (next tick) with the completion value or rejection reason, just like if they were registered before it completed.
"But Tab!", I hear you say, "you could just as easily return an EventTarget object and just fire events! That would accomplish the same thing, but without inventing something new and inconsistent with the rest of the platform!". (Or equivalently, we could standardize some callback argument pattern, or something else.)
You're right, we could! If this was all that Promises offered, they would be a much more difficult sell, and probably not worth the effort.
However, the fact that Promises capture this pattern in a first-class value means that we can push more power into the abstraction. Promises are fundamentally better than the existing patterns for at least 5 strong reasons.
Reason 1: Chaining
Most of the API patterns I rattled off earlier have no convenient way to chain operations. That is, you can't easily schedule a second function to run after your first callback finishes. Most of the time, you have to roll your own chaining somehow, like registering an anonymous function that wraps your two pieces of code.
Promises make this trivial - the return value of .then()
(the callback registration function) is another Promise, which completes when the callback is finished with the callback's return value. (Actually it's even better - see the next section.) This means you can chain a second function just by calling .then()
on this returned value! It's even simpler than it sounds:
someAsyncFunc().then(cb1).then(cb2);
Tada!
Reason 2: More Chaining
There's another type of chaining that is prevalent in async code, which is deeply hated - NESTING HELL. This happens when the first callback finishes up by kicking off another async operation, so you have to pass in another callback to handle that. If you're coding with a lot of anonymous functions (which is totally reasonable, because you're basically just splitting up your literal code into little async chunks), these callbacks end up marching ever rightward, making it difficult to read and understand your program flow.
Promises make this sort of thing trivial, again. Remember in the last section, where I said that the Promise returned by .then()
(let's call this Promise2 - you'll see why) completed when the callback returned, with the callback's return value? That's true, but there's some additional magic involved - if the callback returns a Promise (Promise3), then Promise2 chains itself to Promise3's state. It waits to resolve until Promise3 resolves, and then adopts the same state, accepting or rejecting with the same value. This means that you can do async chaining without the horrible leftward march!
asyncFunc() .then(val=>anotherAsyncFunc()) // only runs when anotherAsyncFunc()'s operation finishes! .then(val=>yetAnotherAsyncFunc()) // only runs when yetAnotherAsyncFunc()'s operation finishes .then(val=>...);
Reason 3: Linear callback growth
For those API variants that let you register both success and error callbacks, the lack of good chaining (see reasons 1 and 2) meant that the number of necessary callbacks would grow exponentially:
oldAsyncFunc(success=>{ return anotherAsync(success=>{ ... },error=>{ ... }); }, error=>{ return yetAnotherAsync(success=>{ ... }, error=>{ ... }); });
That's 6 function declarations - 2 at the top level, 4 nested - and it would be 14 if the same pattern continued one more level. Of course, in practice by this point people are forced into using named functions, just to avoid all the pointless duplication.
In the reasonably common case where the error function is returning some appropriate default value of the same type as the success function, Promises simplify this, so that you don't have to repeat yourself. This means that using anonymous functions stays viable:
newAsyncFunc() .then(success=>anotherAsync(), error=>default) .then(success=>yetAnotherAsync(), error=>default2);
This code has only four function declarations, 2 per level. If it went another level deep, it would only be 6. There's no duplication necessary at all.
Reason 4: Errors are easy to deal with
In normal async code, errors are the devil. Throwing errors and using callbacks simply do not mix, because where you going to put the try/catch? You can't put it around the function that takes the callbacks, because it successfully returns immediately. You can't put it around the callbacks, because they're not called until later. The only way to mix them is to invent your own complex wrapper system, and always use it.
Promises takes care of all this for you, in the same trivial way it does the rest. Recall from Reasons 1 and 2 that whenever you attach a callback to a Promise, it returns a brand new Promise that resolves to the callback's return value, to make chaining easier. Like all Promises, this brand new Promise can take both accept and reject callbacks, and for good reason - if the callback throws, it'll get caught by the Promise and just trigger the reject callback automatically!
Reason 5: Promise combinators
One of the most annoying things to handle when doing async code (besides chaining, more chaining, and errors) is synchronization. If you have a single async operation you want to run some code after, that's fine. If you have two async operations, and you want to wait until they both finish and call some function with both of their results, you're on your own. You have to manually roll some synchronization primitives, where you pass different dummy functions to both, which use some communication channel to tell each other when they're finished and what their return value was, so the last one to finish can finally call your function. If you have two async operations and you just want to respond to the first one of them that finishes, you've got to do that stuff all over again, just with slightly different synchronization code.
Once again, Promises makes this trivial. The Promise interface has several static functions defined on it which combine Promises together into new Promises:
Promise.all()
: if all the passed Promises accept, the output Promise accepts, with an array of their values. If any of them rejects, the output Promise rejects with its reason.Promise.race()
: whichever of the passed Promises is first to accept or reject, the output Promise does the same with the same value.
For example, if we assume there's a Promise-returning XHR API, and you want to make two separate XHRs and run some code when they both return, it's easy:
Promise.all(getJSON(url1), getJSON(url2)).then(function(arr) { // do stuff with arr[0] and arr[1], the JSON results }, function(error) { // handle the error });
Doing the same with today's XHR code, or even jQuery's older (non-Deferred-based) XHR APIs, is non-trivial, but a fun exercise to try if you've never had to do it before. It stops being fun the dozenth time you have to rewrite it in production code, though.
There are potentially even more useful combining operations, but these three should cover the majority of cases.
Promise Conclusion
Hopefully, this has enlightened you as to why the web chose to add Promises, and why some of us are so excited and eager to use Promises everywhere in APIs.
In the DOM world and other closely-related APIs, we're not going to stop using Promises, and the value of Promises grows even more as more specs use them, due to network effects and developer interest. Please do the right thing, and use Promises when appropriate in your own APIs.
In this example: Future.all(getJSON(url1), getJSON(url2)).then(function(arr) {
Is it not better to have the callback function get seperate parameter for each returned value instead of a single array, like so: Future.all(getJSON(url1), getJSON(url2)).then(function(json1, json2) {
Reply?
The persistent "pass an array or multiple arguments" dilemna is resolved in the newest version of Javascript, with its rest arguments, spread operator, and argument destructuring.
If you want to recieve the two arguments directly, just do "function([json1, json2]){...}". This automatically pulls the passed array apart into two named arguments for you.
Reply?
Ok, but ES6 is not even finalized yet. And even after it does, it will take a few more years to be able to use it on the web. While this 'Future' library is something we can use now.
What is the gain in having it as a single array instead of multiple arguments?
Reply?
The gain is that you can easily get all the values, without having to mess around with the
arguments
object.It's easy to pull the values out once you have it, and then you can go about your day. The two methods are functionally equivalent, just more convenient for different types of things.
Reply?
I agree both are functionally equivalent, that is why the API should be designed to cater to the most commonly occurring pattern. And I really see myself using separate parameters all the time. I cant think of any case where array would be better. Even if there is a case, it would be in minority or for library developers.
Having a single array would force me to extract out the values all the time. It isnt a problem for a POC, but for day-to-day programming, it will become a bit annoying.
Also, having multiple parameters is good for readability as you dont need to look inside the function code to determine what arguments are expected aka the signature of the function. This will be good for named functions.
Ofcourse, all of this is my opinion. I am not trying to annoy you, I am as excited about Futures as you are. My last comment on this topic, thanks for the 'Future' :) .
Reply?
The issue is that promises represent a single value, just like function calls can only return a single value. Having them represent multiple values is nonsensical, so instead combinators like
Future.every
orQ.all
return a promise for an array.The real confusion is the
Future
static combinators accept variadic arguments as values, instead of arrays. This is not entirely clear to me.For more information on the parallel between promises and normal function calls, see "You're Missing the Point of Promises".
Reply?
This is such a recognizable problem. I have even built various portions of this pattern to lessen my frustrations (I am sure a lot of people have come up with something like Future.all before in their projects).
With this version I especially like how you can do:
Future.all(someAsyncFunc().then(cb1), anotherAsyncFunc()).then(cb2);
Reply?