Promises are great for giving us a way of writing asynchronous code without having to indent our code but if that's the only thing you're using them for then you're missing the point of promises. Promises are an abstraction that make doing several things easier. They have two properties that make them easier to work with:
an example of promises:
You can see that I wrote the asynchronous methods using normal callback style and converted them to return promises. You could Instead write them directly to use a promise like this:
I simple have to hook up the "then" success function to a single promise, as it can only be fulfilled once. You'll also see that I'm using Aplus.pool as well. This is for error handling, we need to handle the case when all the "horses" break their leg. Now let's race them!
- You can attach more than one callback to a single promise
- values and states (errors) get passed along
Because of these properties it makes common asynchronous patterns using callbacks easy. Here are some cases which may pop up from time to time:
NOTE: For the below I'm going to use APlus which is an implementation of the Promises/A+ spec that we walked through the development of in Promises/A+ - Understanding the spec through implementation. Some of the code below will also be available in the repository under aplus.extras.js. Also we will be using the "Node way" of defining async functions where the first argument takes a function to be run on error and the second function takes the success callback.
Sometimes a library will already have functions that have their own callback chains setup. In this case you only have to wrap the top function if that is all you need.
Converting callback functions to return promises
We don't want to rewrite all our already written functions to return promises and there are lots of useful libraries out there that already use callbacks. In fact we probably don't want to write and functions that we may share in external libraries later to use promises either. This is because there is no native implementation of promises at the moment. Because of the spec, promises are largely compatible but if we're using one implementation for the library the user might be using a different one for their code. Instead it's better to keep using callbacks for the definition of asynchronous functions as they're the base building block and let the user convert them to use promises if they wish. This can easily be acheived and below is an example of how you can convert a node.js style callback function to use the Aplus implementation of promises:
// take a callback function and change it to return a promise
Aplus.toPromise = function(fn) {
return function() {
// promise to return
var promise = Aplus();
//on error we want to reject the promise
var errorFn = function(data) {
promise.reject(data);
};
// fulfill on success
var successFn = function(data) {
promise.fulfill(data);
};
// run original function with the error and success functions
// that will set the promise state when done
fn.apply(this,
[errorFn, successFn].concat([].slice.call(arguments, 0)));
return promise;
};
};
Sometimes a library will already have functions that have their own callback chains setup. In this case you only have to wrap the top function if that is all you need.
Sequential Calls
This is where promises excel. As the return value of a "then" is a promise that gets fulfilled with the value returned by the given function we only have to chain together to get the next value. That's great if your function can return a value directly but if it's asynchronous (which is the whole reason you're using a promise in the first place) then you'll want to return a promise which then gets used as the basis for firing the next chained "then" method.
an example of promises:
var asyncAddOne = Aplus.toPromise(function(err, cb, val) {
setTimeout(function() {
cb(val + 1);
});
});
var asyncMultiplyTwo = APlus.toPromise(function(err, cb, val) {
setTimeout(function() {
cb(val * 2);
});
});
var asyncInverse = APlus.toPromise(function(err, cb, val) {
setTimeout(function() {
if (val === 0) {
return err('value is zero');
}
cb(1 / val);
});
});
var alertResult = function(value) {
alert(value);
};
asyncAddOne(1)
.then(asyncMultiplyTwo)
.then(asyncInverse)
.then(alertResult);
You can see that I wrote the asynchronous methods using normal callback style and converted them to return promises. You could Instead write them directly to use a promise like this:
var asyncAddOne = function(val) {
var promise = APlus();
setTimeout(function() {
promise.fulfill(val + 1);
});
return promise;
};
Error Handling
If you are calling several services and want any error to short circuit then promises excel at this. If doing this with callbacks we only have to declare each function in turn and put our single error handling function at the end. In the previous example we have an inverse function that can call an error function, if we wanted to write this as a promise directly we could have just thrown an error like so:
Though we also have the option of just throwing an error instead which will pass along the value as the error thrown. To add a single error handling function we just need to tack it on to the end of our call:
Note how the first argument is undefined as the second argument is used for the error. We could just have easily put the onError with the alertResult call, but then we wouldn't catch any error from the alertResult function.
var asyncInverse = function(val) {
var promise = APlus();
if (val === 0) {
promise.reject('can not inverse zero');
}
setTimeout(function() {
APlus.fulfill(1 / val);
});
return promise;
};
Though we also have the option of just throwing an error instead which will pass along the value as the error thrown. To add a single error handling function we just need to tack it on to the end of our call:
var onError = function(err) {
console.error(err);
};
asyncAddOne(1)
.then(asyncMultiplyTwo)
.then(asyncInverse)
.then(alertResult)
.then(undefined, onError);
Note how the first argument is undefined as the second argument is used for the error. We could just have easily put the onError with the alertResult call, but then we wouldn't catch any error from the alertResult function.
Pool
Sometimes you want the result of several operations before you continue. Instead of waiting for each one to finish before going on with the next we can save some time by firing them all off at once. Basically we have to catch when a promise is finished (through both it's success and error callbacks) and save it's value to an array. Once they're all done then we can fulfill or reject the promise with the given values. We'll first need some code that can do this for us - here is a method that we can use:
// resolve all given promises to a single promise
Aplus.pool = function() {
// get promises
var promises = [].slice.call(arguments, 0);
var state = 1;
var values = new Array(promises.length);
var toGo = promises.length;
// promise to return
var promise = Aplus();
// whenever a promise completes
var checkFinished = function() {
// check if all the promises have returned
if (toGo) {
return;
}
// set the state with all values if all are complete
promise.changeState(state, values);
};
// whenever a promise finishes check to see if they're all finished
for (var i = 0; i < promises.length; i++) {
(function(index) {
promises[index].then(function(value) {
// on success
values[index] = value;
toGo--;
checkFinished();
}, function(value) {
// on error
values[index] = value;
toGo--;
// set error state
state = 2;
checkFinished();
});
})(i);
};
// promise at the end
return promise;
};
Now all we need to do is pass in all the promises we need:
In the above getName and getAddress both return promises - though it is possible to tweak the pool function logic so that any non-promise return value is just passed through directly. There are some implementations that do this such as jQuery's $.when
We've given it a 5% chance of error (or if you're like me and pretending it's a horse race - breaking it's leg and not finishing). Now we need to write a function that will run them all:
Aplus.pool(
getName(),
getAddress()
).then(function(value) {
var name = value[0];
var address = value[1];
alert(name + ' lives at ' + address);
}, function() {
alert('unable to retrieve details');
});
In the above getName and getAddress both return promises - though it is possible to tweak the pool function logic so that any non-promise return value is just passed through directly. There are some implementations that do this such as jQuery's $.when
Some fun
Okay, there are some common patterns. Let's see if we can build on top of those. Let's say I'm racing some functions. The fastest to return wins the race. They also may error. Let's write such a function that will return a promise that is only fulfilled or rejected after a timeout:
var racer = function(err, success, value) {
setTimeout(function() {
if (Math.random() < 0.05) {
err(value);
} else {
success(value);
}
}, Math.random() * 2000);
};
var promiseRacer = Aplus.toPromise(racer);
// return the value of the first succesful promise
Aplus.first = function() {
// get all promises
var promises = [].slice.call(arguments, 0);
// promise to return
var promise = Aplus();
// if all promises error out then we want to return an error
Aplus.pool.apply(Aplus, promises).then(undefined, function(value) {
promise.reject(value);
});
// when there is a success we want to fulfill the promise
var success = function(value) {
promise.fulfill(value);
};
// listen for success on all promises
for (var i = 0; i < promises.length; i++) {
promises[i].then(success);
}
return promise;
};
I simple have to hook up the "then" success function to a single promise, as it can only be fulfilled once. You'll also see that I'm using Aplus.pool as well. This is for error handling, we need to handle the case when all the "horses" break their leg. Now let's race them!
Aplus.first(
promiseRacer('Binky'),
promiseRacer('Phar Lap'),
promiseRacer('Sea Biscuit'),
promiseRacer('Octagonal'),
promiseRacer('My Little Pony'),
).then(function(value) {
alert(value + ' wins!');
}, function() {
alert('no function finished');
});
And there you have it, a simple racing game made with promises.
Again, an excellent write-up. You've made promises easy to understand even to someone as dumb as me.
ReplyDeleteIs line 17 of the Aplus.first definition correct? Aplus.pool.apply(promises) vs Aplus.pool.apply(Aplus, promises)?
good catch - I'll update the code
DeleteThis comment has been removed by a blog administrator.
DeleteI might be pedantic here, but I'd change:
ReplyDeletevar promises = [].slice.call(arguments, 0);
to
var promises = Array.prototype.slice.call(arguments, 0);
Since it avoid creating an empty array each time for no reason.
Also, you might want to consider
var values = new Array(promises.length);
As a way to create an array (kinda silly imo)
Those are all syntactic things, as for the content - very nice :)
Both good suggestions. I just did that as shorthand. Usually I've got an args to array or slice function that I use but was just putting it out there in short hand - I'll fix the github repo
DeleteFor the second point the reson I'm using the new Array(length) is so that the array will already have it's length given which should be faster (see http://jsperf.com/array-init-with-length)
One of the best explanations of Promises that I have seen! Thank you!
ReplyDeletecould you explain "you're missing the point of Promise" post about jQuery isn't compliant the error handle? Show some code is better:) thx
ReplyDeleteFor those who are waiting for a sync (non-busy) sleep(), here it is: Hypnotic: Let JavaScript Sleep()!
ReplyDeletehttp://coolwanglu.github.io/hypnotic/web/demo.html
Hi,
ReplyDeleteCan you help me with my case scenario?
I have array of some data, let's say Cities. In the loop I execute some async call (let's say to bring the streets) to get the promise. I mark the city object as done or fail depends on promise status. Once all cities' promises are done and I want to execute function that render the data depends on the status of the promise, either true or false.
Here what I have tried:
---------------------------------------------------
var promises = [];
$.each(Cities, function (key, value) {
var city = this;
var streets = .... ajax call
promises.push(streets);
streets.done(function (result) { city.succeed = true; }).fail(function () { city.succeed = false; });
});
$.when.apply($, promises).always(ShowCitiesWithStreets);
This scenario is partially works. The problem with this one is once some of the promises fail ShowCitiesWithStreets execute immediately. If everything is done it is working. I need to execute ShowCitiesWithStreets always at the end of the loop, either it was failed or succeeded.