In this post I explore the patterns available for working with asynchronous code in a manageable way.
Let’s start by looking at a simple, synchronous API and some code that uses it.
Hopefully, you find the above code easy to follow. If we make the API asynchronous, we’ll need to update the code. Ideally, we’d like to avoid drastically changing the structure or risk obfuscating the algorithm.
The naive approach is to use callbacks. Each asynchronous function takes an additional argument, a callback, which it will call with the result of the operation.
How does our code example look with callbacks?
Unfortunately, as you can see above, callbacks don’t scale well to higher levels of complexity. This anti-pattern of nesting is known as the “Pyramid of Doom”.
Instead of passing a callback to each method, the methods can instead return
Promises, which are objects
that allows us to register one or more callbacks using the
returns another Promise, which allows us to chain multiple Promises together rather than nesting them.
How does our code example look with Promises?
That isn’t much of an improvement over the version that uses callbacks. What’s going on here? It turns out that Promises don’t completely solve the “Pyramid of Doom”. In our code example, the results of earlier asynchronous calls need to be combined at the end, forcing us to nest the callbacks to capture the earlier results in the closure. This prevents us from benefiting from the best feature of Promises - the chaining.
Generators are another
exciting feature coming in ES6. They’re already supported
in the latest version of Firefox, Chrome and Opera and in Node.js when running with a flag. Generators are functions
that can stop and resume their execution. This is done with the new
yield keyword. If you’d like to know
more about generators check out these excellent
Combining Generators with a bit of library magic gives us a way of writing code that is very close to the synchronous
yield we can make the code wait for a Promise to be fulfilled and continue once the result is
available. It’s as if the rest of the code in the function is wrapped up in the fulfilled-handler for that Promise.
The library magic required can be very simple. However, as an alternative you may wish to use one of the many libraries as they support more features, such as yielding on an array of Promises to await all the Promises to be fulfilled. We’ll use task.js here.
This is a huge improvement over the other versions. Gone is the additional control flow and nesting. The boilerplate has
been reduced to the
The wrapped function will return a Promise. This makes it easy to compose generators and mix them with existing code that returns or expects Promises. When trying this pattern out I was surprised by how easily it integrated into my existing codebase.
Looking even further into the future, ES7 includes a proposal for adding two keywords: async and await. Developers with experience of C# will immediately recognise the syntax.
This version is similar to the generators version, but the intent of code is arguably more clear.