JavaScript is mostly single-threaded, code cannot be executed while any other operation is currently executing. This has lead to the JavaScript community shunning long, blocking operations and moving to asynchronous APIs. Asynchronous operations are non-blocking and signal their completion via some other channel. New libraries and APIs are increasingly asynchronous and hence developers are increasingly required to deal with asynchronous code.
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.
Callbacks
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.
Synchronous version
Asynchronous version
Callbacks are straightforward and work well for simple cases. They work in all versions of JavaScript and are using extensively by Node.js’s standard library.
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”.
Promises
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 Promise.then
method. Promise.then
returns another Promise, which allows us to chain multiple Promises together rather than nesting them.
Promises are an excellent addition to JavaScript. They’re supported natively in ES6 and, as they don’t require any additional syntax, can be easily polyfilled. I recommend using Promises over callbacks for anything but the most trivial cases.
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
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
descriptions.
Combining Generators with a bit of library magic gives us a way of writing code that is very close to the synchronous
code. Using 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 yield
keyword.
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.
Unfortunately, it’s not possible to polyfill generators as they introduce new syntax. If you need to support older JavaScript environments, then you can use one of the excellent transpilers. These tools take the ES6 code and compile it to ES5-compatible JavaScript. Huzzah!
Async/await
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.
Traceur supports this proposed feature, so if you’re going to transpile the code anyway you may consider using this syntax. Be warned that tools like jshint don’t yet support the syntax.
Conclusion
Dealing with asynchronous code in JavaScript can be a challenge and the single-threaded nature of the language makes this type of code very common. Thankfully, JavaScript is evolving to help developers solve this challenge. Generators offer a good layer above Promises for writing clean, synchronous-like code. Native support for them has already arrived or is arriving soon and transpilers make it easy to support older environments.