Callbacks, Promises, Signals and Events
Sometimes I get involved in discussions on twitter and Facebook related to development. This week I ended up sending a link to an old(ish) twitter thread explaining when to favor Promises/Callbacks and when to use Signals/Events for asynchronous operations. I think this topic deserves further explanation.
Callbacks
Callbacks are commonly used when you have an asynchronous operation that should notify the caller about its’ completion:
var toppings = {cheese: true, bacon: true}; function orderPizza(data, done) { // send info to server (which might take a few milliseconds to complete) xhr({ uri: '/order/submit', data: data, complete: function(status, msg, response) { if (status !== 200) { // notify about error done(new Error(status +': '+ msg)); } // notify about success done(null, response); } }); } // the method "showOrderStatusDialog" will be called after we get the response // from the server orderPizza(toppings, showOrderStatusDialog); function showOrderStatusDialog(err, response) { if (err) { showOrderFailure(err); } else { showOrderSuccess(response); } }
Callbacks are great for cases where you have an action that triggers a direct reaction (eg. animation, ajax, batch processing).
PS: technically, a callback is any function that is passed as an argument to another function and that is executed later, be it synchronously or asynchronously. But lets keep the focus at async actions that requires a response.
Promises
Promises are a replacement for Callbacks to help you deal with composition of multiple async operations while also making error handling simpler in some cases.
function orderPizza(data) { // the xhr in this case returns a promise and handle the error logic // internally; it shifts the complexity to the xhr implementation instead // of spreading it through your app code return xhr({ uri: '/order/submit', data: data }); } // first callback of `then` is success, 2nd is failure orderPizza(toppings).then(showOrderSuccess, showOrderFailure);
There are 2 really cool things about promises:
- they are only resolved once;
then
method returns another promise and you can return data from the
callback to map the value received by the next callback in the chain;
Since promises are only resolved once they can be used for smart caching or for cases where you might have multiple calls trying to execute same action (which should only happen once).
var _cache = {}; function getOrderInfo(id) { if (! _cache[id]) { _cache[id] = xhr({ uri: '/order/'+ id }); } return _cache[id]; } getOrderInfo('123').then(...); // .... later in the code // since we already asked for the order "123" info it won't do another XHR // request, it will just use the cached promise and execute callbacks whenever // the promise is resolved; if it was already resolved it will execute callback // on the nextTick getOrderInfo('123').then(...);
And you can compose multiple async operations.
orderPizza(toppings) // if `processStatus` returns a promise the `showOrderSuccess` will only be // called once the promise is "fullfilled", this makes composition of // multiple async operations very easy; // if the value returned by `processStatus` is not a promise it will be // passed directly to the `showOrderSuccess` (similar to Array#map) .then(processStatus) .then(showOrderSuccess, showOrderFailure);
PS: There is a proposal to add Promises to the EcmaScript spec, but it’s generating a lot of discussions – some people think this kind of abstraction should live in “user land”.
Signals
Signals are not a replacement for Callbacks/Promises! They have a totally different purpose. Signals are used as a way of communication between objects. They are mainly used for cases where you need to react to actions that you are not responsible for triggering. These events usually happens multiple times during the application lifetime and might be dispatched at random intervals.
// you register listeners to each discrete signal and `delivery` object doesn't // need to know about the existence of your object (or how many objects are // listening to a specific event) delivery.leftBuilding.add(userNotifications.dispatched); delivery.arrivedAtLocation.add(userNotifications.arrival); delivery.succeed.add(userNotifications.success); delivery.failed.add(userNotifications.failure);
Anything that happens multiple times and/or that might be triggered at random intervals should be implemented as Signals/Events.
The main advantage of Signals over Events is that it favors composition over inheritance. Another benefits are that discoverability is higher (easier to identify which events the object dispatches), and it also avoids typos and/or listening to the wrong event type; since trying to access an nonexistent property throws errors - which usually helps to spot errors earlier and simplify the debug process.
Events
EventEmitters are basically an Object that creates/dispatches multiple Signals types on-demand and usually use Strings to define the message name.
The biggest advantage of Events over Signals is that you can dispatch dynamic events during runtime - suppose you want a different event type for each change on your Model class.
node.js uses EventEmitter internally a lot but creates some callback-like APIs to abstract/simplify the process, like the http.createServer
method:
http .createServer(respondToRequest) .listen(1337, '127.0.0.1');
Is exactly the same as:
var server = http.createServer(); server.on('request', respondToRequest); server.listen(1337, '127.0.0.1');
So even tho it looks like http.createServer
takes a regular done Callback (like described on my first examples), it is in fact an event listener. - I do not recommend this kind of API since at first look user would think that http.createServer
argument would be executed after its “creation” and not at each request.
I hope it’s clearer when/how to use the 4 patterns!
PS: I usually favor Signals over EventEmitter since I believe it has some very strong benefits. I’m planning to code and release v2.0 of js-signals with some improvements, slightly different API and killing some of the bad features. Feedback and pull requests are highly appreciated!
Edit 2014/01/17: improved Signals description to describe better the use cases and advantages/differences.