Awaiting the future of JavaScript in V8
On the evening of Monday, May 16th, 2016, we have made history. We've landed the initial implementation of "Async Functions" in V8, the JavaScript runtime in use by the Google Chrome and Node.js. We do these things not because they are easy, but because they are hard. Because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one we are willing to accept. It is very exciting to see this, roughly 2 months of implementation, codereview and standards finangling/discussion to land. It is truly an honour.
To introduce you to Async Functions, it's first necessary to understand two things: the status quo of async programming in JavaScript, as well as Generators (previously implemented by fellow Igalian Andy)
Async programming in JavaScript has historically been implemented by callbacks. window.setTimeout(function toExecuteLaterOnceTimeHasPassed() {}, ...)
being the common example. Callbacks on their own are not scalable: when numerous nested asynchronous operations are needed, code becomes extremely difficult to read and reason about. Abstraction libraries have been tacked on to improve this, including caolan's async package, or Promise libraries such as Q. These abstractions simplify control flow management and data flow management, and are a massive improvement over plain Callbacks. But we can do better! For a more detailed look at Promises, have a look at the fantastic MDN article. Some great resources on why and how callbacks can lead to utter non-scalable disaster exist too, check out http://callbackhell.com!
The second concept, Generators, allow a runtime to return from a function at an arbitrary line, and later re-enter that function at the following instruction, in order to continue execution. So right away you can imagine where this is going --- we can continue execution of the same function, rather than writing a closure to continue execution in a new function. Async Functions rely on this same mechanism (and in fact, on the underlying Generators implementation), to achieve their goal, immensely simplifying non-trivial coordination of asynchronous operations.
As a simple example, lets compare the following two approaches:
function deployApplication() {
return cleanDirectory(__DEPLOYMENT_DIR__).
then(fetchNpmDependencies).
then(
deps => Promise.all(
deps.map(
dep => moveToDeploymentSite(
dep.files,
`${__DEPLOYMENT_DIR__}/deps/${dep.name}`
))).
then(() => compileSources(__SRC_DIR__,
__DEPLOYMENT_DIR__)).
then(uploadToServer);
}
The Promise boiler plate makes this preit harder to read and follow than it could be. And what happens if an error occurs? Do we want to add catch handlers to each link in the Promise chain? That will only make it even more difficult to follow, with error handling interleaved in difficult to read ways.
Lets refactor this using async functions:
async function deployApplication() {
await cleanDIrectory(__DEPLOYMENT_DIR__);
let dependencies = await fetchNpmDependencies();
// *see below*
for (let dep of dependencies) {
await moveToDeploymentSite(
dep.files,
`${__DEPLOYMENT_DIR__}/deps/${dep.name}`);
}
await compileSources(__SRC_DIR__,
__DEPLOYMENT_DIR__);
return uploadToServer();
}
You'll notice that the "moveToDeploymentSite" step is slightly different in the async function version, in that it completes each operation in a serial pipeline, rather than completing each operation in parallel, and continuing once finished. This is an unfortunate limitation of the async function specification, which will hopefully be improved on in the future.
In the meantime, it's still possible to use the Promise API in async functions, as you can await
any Promise, and continue execution after it is resolved. This grants compatibility with numerous existing Web Platform APIs (such as fetch()
), which is ultimately a good thing! Here's an alternative implementation of this step, which performs the moveToDeploymentSite()
bits in parallel, rather than serially:
await Promise.all(dependencies.map(
dep => moveToDeploymentSite(
dep.files,
`${__DEPLOYMENT_DIR__}/deps/${dep.name}`
)));
Now, it's clear from the let dependencies = await fetchNpmDependencies(); line that Promises are unwrapped automatically. What happens if the promise is rejected with an error, rather than resolved with a value? With try-catch blocks, we can catch rejected promise errors inside async functions! And if they are not caught, they will automatically return a rejected Promise from the async function.
function throwsError() { throw new Error("oops"); }
async function foo() { throwsError(); }
// will print the Error thrown in `throwsError`.
foo().catch(console.error)
async function bar() {
try {
var value = await foo();
} catch (error) {
// Rejected Promise is unwrapped automatically, and
// execution continues here, allowing us to recover
// from the error! `error` is `new Error("oops!")`
}
}
There are also lots of convenient forms of async function declarations, which hopefully serve lots of interesting use-cases! You can concisely declare methods as asynchronous in Object literals and ES6 classes, by preceding the method name with the async
keyword (without a preceding line terminator!)
class C {
async doAsyncOperation() {
// ...
}
};
var obj = {
async getFacebookProfileAsynchronously() {
/* ... */
}
};
These features allow us to write more idiomatic, easier to understand asynchronous control flow in our applications, and future extensions to the ECMAScript specification will enable even more idiomatic forms for writing complex algorithms, in a maintainable and readable fashion. We are very excited about this! There are numerous other resources on the web detailing async functions, their benefits, and perhaps ways they might be improved in the future. Some good ones include this piece from Google's Jake Archibald, so give that a read for more details. It's a few years old, but it holds up nicely!
So, now that you've seen the overview of the feature, you might be wondering how you can try it out, and when it will be available for use. For the next few weeks, it's still too experimental even for the "Experimental Javascript" flag. But if you are adventurous, you can try it already! Fetch the latest Chrome Canary build, and start Chrome with the command-line-flag --js-flags="--harmony-async-await"
. We can't make promises about the shipping timeline, but it could ship as early as Chrome 53 or Chrome 54, which will become stable in September or October.
We owe a shout out to Bloomberg, who have provided us with resources to improve the web platform that we love. Hopefully, we are providing their engineers with ways to write more maintainable, more performant, and more beautiful code. We hope to continue this working relationship in the future!
As well, shoutouts are owed to the Chromium team, who have assisted in reviewing the feature, verifying its stability, getting devtools integration working, and ultimately getting the code upstream. Terriffic! In addition, the WebKit team has also been very helpful, and hopefully we will see the feature land in JavaScriptCore in the not too distant future.