JavaScript Asynchronous Architectures: Events vs. Promises

I can’t go a single week without reading another article talking about promises. I’m not talking about when you promise your child that you’ll be there for his baseball game. I’m talking about a JavaScript concept that makes it simple to react to the completion of asynchronous actions without indenting ten levels when you need to perform one asynchronous action after another. While working on a Backbone application, I tried to use promises in my main asynchronous processes, and I’m not sure it measures up to my previous event hub architecture. Let’s compare!

Before I get into why I prefer the event hub, at least for my own application, I’d like to go over each of the methodologies a little so you can understand them better, just in case you haven’t heard much about them.

Promises and the Deferred Object

These have become all the rage these days, and for good reason. Rather than creating a function that allows you to send in a callback that is run when an action finishes, the function returns a promise object. Upon this promise object you can now call something like done and send a callback into it that runs when/if the promise reaches a “done” state. A promise is created by a Deferred object. First you create a Deferred object and then return deferred.promise(), which gives you your promise object. The deferred is used to update the status of the asynchronous action. For example, when the action is completed you would call deferred.resolve(). When this is called, the promise will run all of the callbacks that were registered to it through the done, then, and always methods.

Let’s look at some examples to compare traditional callbacks to using promises. These are taken from the Parse Blog because they do a pretty decent job of demonstrating the usefulness of using promises:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Traditional example using nested 'success' callbacks
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.
}
});
}
});
}
});
// Another traditional example using plain callbacks (without 'success')
Parse.User.logIn("user", "pass", function(user) {
query.find(function(results) {
results[0].save({ key: value }, function(result) {
// the object was saved.
});
});
});

As you can see, in either case you end up nesting deeper and deeper with each action you perform. Here’s what it would look like if all three of the methods used in the above example returned promises.

1
2
3
4
5
6
7
8
// Promises example using 'then'
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.
});

As you can see, no matter how many actions we perform, the indentation only goes one level deep. The way it is written, it reads quite easily: “login, then find, then save, then… whatever we do when it’s saved.”

To do the chaining as it is done above, we need to use then because then returns a new promise that is resolved either when the callback function returns a non-promise or the promise that the callback function returns is resolved.

For more on promises, you should check out the Q library and its documentation. jQuery also has a promises implementation, but as noted in an article by Domenic Denicola, it’s broken a bit. I still tend to use jQuery’s implementation because I don’t need an additional library and thus far it suits my needs.

Events and the Event Hub

I’ve already talked about using Event-Based Architectures, but I’ll still touch on it a bit more here. Rather, I’m going to give more concrete examples here. Using the event-based architecture is similar to the traditional callback way of doing things, except that you register the callback beforehand and it persists for use when an event is triggered again later. We’re going to use Backbone’s event system because it is similar to what I’m trying to use in my application. If you’re not familiar with Backbone, I suggest going through my screencast series on Backbone, but beware that newer versions of Backbone make this somewhat obsolete. Don’t worry, I’ll put together something to show you all the changes after 1.0 is released.

The example below is part of an application that starts and stops servers that run on the back end. The client app makes calls to the back end to start a server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// The view will do something when a model finishes doing something asynchronous
ServerView = Backbone.View.extend({
initialize: function() {
this.model.on('started', this.serverStarted, this);
},
serverStarted: function() {
// change something about view to indicate to users that
// the server is running
},
startServer: function() {
this.model.start();
},
...
});
Server = Backbone.Model.extend({
initialize: function() {
// React when the application lets us know a server was started
AppEvents.on('server:started', this.started, this);
},
start: function() {
// Using a utility class, make a call to the back end to start the server.
// When a success message comes back from the back end, the utility will
// trigger an application-wide event to inform the entire system that a
// server has been started.
communicator.startServer(this);
},
started: function(serverID) {
if (serverID == this.get('id')) {
// trigger the 'started' event
this.trigger('started', this);
}
},
...
});
server = new Server();
view = new ServerView({model:server});

There’s a lot more to this example even though it essentially only does one thing. One thing I didn’t mention in the code is how the view’s startServer method is called. We’ll assume it’s done via user interaction, such as clicking a “start server” button.

As you can see, in the initialize functions of each of the above ‘classes’, we register our event handlers. This only happens once, so even if we start (and stop – even though I didn’t show code for stopping) a server multiple times, the handlers already exist and are ready to handle any event.

The Comparison

Do you see the awesome differences that events made?

  1. The start functions on the view and model are very small and only do one thing: start the server (according to their respective abstractions).
  2. The whole system is now able to know about the server starting. Nothing needs to have knowledge of any of the individual server models, but can still react when one of them starts.

The code examples for the promises pretty much showed some procedural programming. This is all well and good, but what about object-oriented programming? Objects’ methods need to be succinct, and if a single method is handling everything that is shown in that example, it may be a good idea to refactor.

I also like the event-based architecture better in this instance because in my real application I’m using WebSockets to tell the back end to start the server. WebSockets are already event-based, so it seems to make sense to use events for handling these sorts of things.

Finally, in this example, we have several layers of abstraction (plus one more in my real application), so for the most part, I’m just passing the promise all the way back and no one is using it until it gets to the view, in which case the promise would be used to do more than start the server, so it shouldn’t be in the startServer method.

In all fairness, you can send a callback function with WebSockets (at least with Socket.IO; I’m not sure about WebSockets themselves) and use the callback function to resolve the promise as well as alert the rest of the system. In other words, you can use a combination of promises and events, but this makes it difficult to decide which is a better practice in each individual situation. However, as my application grows, I may end up needing to do just that. Time will tell.

Conclusion

The reason I wrote this article is because I recently spent much time arguing with myself on this very issue. How should the asynchronous actions be implemented in my application? I thought about it a lot, and even as I wrote this, I thought about it even more. They’re both great techniques and deserve to be looked at. In the end, this article is more to get you thinking about your asynchronous design decisions than it is to argue for one methodology over the other. God bless and happy coding!

Author: Joe Zimmerman

Author: Joe Zimmerman Joe Zimmerman has been doing web development ever since he found an HTML book on his dad's shelf when he was 12. Since then, JavaScript has grown in popularity and he has become passionate about it. He also loves to teach others though his blog and other popular blogs. When he's not writing code, he's spending time with his wife and children and leading them in God's Word.