A few months ago I answered a question on StackOverflow that got a lot of attention, especially after I’ve submitted it to Hacker News. The question was “Why are callbacks more tightly coupled than promises?”. I did not ask that question, but the current highest voted answer is mine, however as you can read from the comments on hacker news, there’s a bit of criticism, and some of it justified.
I’ve been meaning to write this post for a while. It describes
- What is a promise
- What problems do promises solve
- What can you do with promises that you can’t with callbacks
It addresses some of the criticisms I got from the hacker news’ comments, and since it’s way too long for stackoverflow I’ve decided to put it here.
I won’t argue that using promises is superior to using callbacks, I’ll leave that to your judgement. So here we go, what is a promise?
Promises are objects which represent the pending result of an asynchronous operation
Martin Fowler (source)
They are also know as deferreds or futures.
The important thing here is that promises are objects, you can hold on to a reference to them, return them in functions and pass them as arguments to other functions, basically anything you can do with an object.
More importantly, you can combine them and create new promises from old ones (more on that shortly). But first let’s compare signaling the completion of an asynchronous method with a callback and with a promise.
Promise vs Callback
//The $.ajax returns a promise. You can either use it or supply a callback function for complete and/or error
var performAjaxRequest = function(onDoneCallback){
return $.ajax(someUrl, {
complete: onDoneCallback
});
};
var performRequestUsingCallback = function(){
performAjaxRequest(function(){ alert('Complete callback');});
};
var performRequestUsingPromise = function(){
var performingRequest = performAjaxRequest();
performingRequest.done(function(){
alert('Promise done');
});
};
I’ve added the performAjaxRequest
function so it would match this example in jsFiddle using a fake ajax request.
Before I delve into the details of this example, let me first highlight a point that might be confusing. In jQuery there is the concept of Promise and Deferred, and they are not exactly the same thing (if you look at the jsfiddle example, you’ll see a deferred being created). They both represent an asynchronous operation, however a Deferred exposes methods to signal its completion (.resolve
) or its failure (.reject
). A promise is exactly the same thing without those methods. You can get hold of a promise by calling deferred.promise().
Things to note in this example:
- You can call
.done
on a promise, the callback you supply will run after the asynchronous operation that the promise represents finishes (here’s the doc page for .done). This is implemented by calling.resolve
or.resolveWith
on the jQuery Deferred object.
When .resolve
is called on a deferred it is possible to supply parameters, for example deferred.resolve(42)
. This will trigger a call on .done
with the value 42, i.e., .done(function(x){ //x is 42})
. For example, in $.ajax
the deferred is resolved with (data, textStatus, jqXHR)
.
You can also call .fail
, .always
and .then
on a promise:
.fail
is called when the.reject
method is called on the deferred object that represents the promise..always
is called when.reject
or.resolve
are called in the deferred object.then
is special. It can do two things (and this is not clear in the jQuery documentation and depends on the version of jQuery), but the important thing to be aware is that.then
always returns a new promise. The examples later in the post will make this clear.
This last example is not terribly interesting, however notice that when using promises the handling of the completion of the operation can be specified after the invocation of the asynchronous operation, for example:
showLoadingScreen();
var loadingData = loadData().done(refreshDataOnScreen, updateCounters)
.fail(logError, displayErrorMessage)
.always(hideLoadingScreen);
loadingData.done(doSomethingWithTheData);
NOTE: gettingData.done(doSomethingWithTheData);
is shorthand for gettingData.done(function(data){doSomethingWithTheData(data);});
.
The major difference between using a callback and a promise that this example highlights is that with the callback you only have one opportunity to provide the function that runs on completion (or error), whereas with a promise you can add several functions at different points in your code.
NOTE: when done, fail and always are supplied more than one function, those functions are executed in the order they are supplied (also, promise.done(fn1, fn2)
is the same as promise.done(fn1).done(fn2)
).
Another thing that you can do with promises that is enabled by them being objects is to create promises from promises. For example:
$.when(loadDataFromSourceA, loadDataFromSourceB).done(refresh);
$.when
returns a new promise that is “resolved” when all its promises are resolved, and is “rejected” as soon as one of its promises is rejected (in the example when’s promises are loadDataFromSourceA and loadDatafromSourceB).
Although done, fail and always return promises, when is the first example where a new promise is returned, another example is .then
.
.Then
.then
is described in the documentation as a way to filter the result of a promise, for example:
var newPromiseCreatedWithThen = loadData().then(function(data) { return data.length});
newPromiseCreatedWithThen.done(function(dataLength) {
//dataLength is the length of data in the original promise
};);
However, that’s not its most useful feature. You can return a new promise from from the callback you supply to.then
. If you do that, it won’t act as a “filter”. It will act as a way of chaining asynchronous calls, i.e.:
loadDataFromA().done(refreshDataFromA)
.then(function(dataFromA){ return loadDataFromB(dataFromA) })
.done(refreshDataFromB);
The refreshDataFromA
function will run as soon as loadDataFromA
finishes. The call to loadDataFromB
will start after loadDataFromA
finishes, and refreshDataFromB will run at the end, after loadDataFromB
finishes. This is useful in scenarios where you need the result of an asynchronous operation in order to perform the next asynchronous operation. When used this way the function name (.then
) actually makes sense (here’s an example that compares .done
and .then
and shows that returning a promise from .then
is given “special” treatment).
This behavior is actually described in the documentation for .then:
These filter functions can return a new value to be passed along to the promise’s .done() or .fail() callbacks, or they can return another observable object (Deferred, Promise, etc) which will pass its resolved / rejected status and values to the promise’s callbacks
However, as you can see, it’s very easy to miss (and in my opinion, not very clear).
A last note about .then
. It might be tempting to say that it provides a solution to “callback hell”, for example:
$.ajax("loadDataUrl", {
complete: function() {
$.ajax("loadMoreData", {
complete: function() {
....
Although it is true that if you use .then
you won’t have nested callbacks, it is also true that there are ways of not rewriting this last example withough promises and nested callbacks. In fact, anything you can do with promises, you can do without.