In this post we'll explore an enlightened approach to managing control flow in asynchronous javascript applications. We'll look at powerful new ES6 patterns and the happy future that awaits
with ES7.
So we are all on the same page, when we discuss control flow we mean the following:
Control flow refers to the specification of the order in which the individual statements, instructions or function calls of an imperative program are executed or evaluated.
Why it's an issue
Writing synchronous code in Javascript is a breeze.
The code executes in the order it is written. As long as you're aware of hoisting and a few other gotchas, what you see is what you get. Beautiful.
var invitations,
newUser = createUser(name),
friends = getFacebookFriends(name),
if (friends) {
invitations = inviteFacebookFriends(friends);
}
On the other hand, writing asynchronous code can be pretty tricky.
If you've worked with Javascript for any amount of time you likely remember when callbacks were the only tool in the javascript engineer's toolbox:
createUser(name, function() {
getFacebookFriends(name, function(friends) {
if (friends) {
inviteFacebookFriends(friends, function(invitations) {
// ad infinitum
});
};
});
});
This pattern is aptly referred to as Callback hell. This is the antithesis of intuitive control flow. Not to mention, the bottom half of our code more closely resembles Scheme than Javascript.
Things improved significantly when promises hit the scene. With a bit of refactoring, our little example now looked like this:
createUser(name)
.then(getFacebookFriends)
.then(function(friends) {
if (friends) {
return inviteFacebookFriends(friends);
} else {
throw new AntiSocialError('No friends to invite');
}
})
.catch(displayError);
Not bad, right? While it is worlds better than nested callbacks, it is not without its weaknesses. Imagine a scenario where each of those methods needed to implement complex conditional logic. We have two options on how to refactor:
- Sacrifice readability and push our complex logic into the promise chain.
- Reduce maintainability and wrap our logic in concrete methods which are consumed by the chain.
In either case the promise chain pattern begins to fall short for anything that goes beyond simple decoration and linear sequences.
An enlightened approach
With the recent approval of the ES6 ES2015 standard, and the promising async
feature in ES7, we have a whole set of exciting new tools in our toolbox.
Let's begin by looking at the brilliant new ES7 proposals, then we'll circle back to leveraging new powerful new ES2015 patterns today.
ES7 Async Functions
async
is an ES7 proposal which aims at reducing the complexity of working with asyncronous operations. The general gist is that one can await
the resolution of a promise within an async
function. Let's look at an example:
async function setupNewUser(name) {
var invitations,
newUser = await createUser(name),
friends = await getFacebookFriends(name);
if (friends) {
invitations = await inviteFacebookFriends(friends);
}
// some more logic
}
Notice how similar it looks to our synchronous example above? The only hints that the functions we are calling perform asynchronous operations are the little async
and await
keywords.
What is happening is that we are awaiting the result of our asnyc functions. Once those promises resolve, they are assigned to our variables and execution continues. Our control flow is once again transparent.
However, async
and await
are still a ways off, so how do we leverage this powerful pattern today?
ES2015 Promises and Generators
You don't have to wait until ES7 is ratified to be an async rockstar. There are several feather-light utilities available which will give you much the same functionality, such as Mozilla's task.js and TJ Holowaychuk's co, to name a few.
How do they work you ask? Simple! They use ES2015 generator functions and promises to achieve control flow nirvana.
You will recall that a generator is a special function whose execution can pause and be resumed at many points. It does so by evaluating a yield
expression which can resume execution of the generator after some indeterminate amount of time.
We'll rewrite our example using task.js, but you could just as easily use another utility.
function setupNewUser(name) {
return spawn(function*() {
var invitations,
newUser = yield createUser(name),
friends = yield getFacebookFriends(name);
if (friends) {
invitations = yield inviteFacebookFriends(friends);
}
}
}
These small utilities work by yielding to a function which returns a promise
. When that promise
resolves, it calls the generator's next()
function, resuming execution.
With those simple tools and an ES2015 transpiler in hand you are now empowered to write cleaner, simpler, more maintainable asynchronous javascript!