ES7 Async/Await

presented at Brookyln.js by @boucher

In the beginning...

The common pattern for writing webservers at the time was to use processes or threads for concurrency. Some people were trying to build evented models that would work within these existing environments, but it wasn't easy.

Ryan Dahl had an important insight: if the core libraries and really the entire ecosystem isn't designed to be asynchronous/evented, you'll constantly run into blocking code that destroys performance.

Thou Shalt Be Async

And so this was a guiding principle of node and the node ecosystem. And also a source of much criticism. Both then and now, a common complaint about node is "callback hell."

You already know what this is, but there's an example on the next slide.

function handler(request, response) { User.get(request.user, function(err, user) { if (err) { response.send(err); } else { Notebook.get(user.notebook, function(err, notebook) { if (err) { return response.send(err); } else { doSomethingAsync(user, notebook, function(err, result) { if (err) { response.send(err) } else { response.send(result); } }); } }); } }) }

Promises

There are lots of opinions about how to deal with this, but ultimately, at least for the purposes of this talk, there's one clear winner: Promises.

Promises are actually pretty simple: they are an object that represents a value which will eventually be available. It might already be available, or it might be ready in a little while.

You interact with promises by passing a callback to its then function. That's why they are sometimes called "thennables."

Here's a straightforward example of a callback:

require("request") .get("http://www.google.com", function(err, response) { console.log(response.body) })

And a promise version. Of course it's very similar. The main difference is we're passing our callback to then.

require("popsicle") .get("http://www.google.com") .then(function(response) { console.log(response.body); })

Here's a more complicated example with callbacks. You can start to see the nesting problems, and the overhead of error handling.

require("request") .get("http://www.google.com", function(err, response) { if (err) { console.error(err); } else { require("fs") .writeFile("google.html", response.body, function(err) { if (err) { console.error(err); } else { console.log("wrote file"); } }) } })

And again the promise version. Instead of nesting, we get a straightforward linear progression of events. Get the URL, then write a file, then log. And we can handle errors more like regular synchronous javascript.

require("popsicle").get("http://www.google.com") .then(function(response) { return require("fs-promise").writeFile("google.html", response.body); }) .then(function() { console.log("wrote file"); }) .catch(function(err) { console.log(err); })

This is a promisified version of our first "callback hell" example. It's much cleaner, but still requires lots of extra syntax and awkward workarounds for the scopes introduced by new functions.

function x(request, response) { var user, notebook; User.get(request.user) .then(function(aUser) { user = aUser; return Notebook.get(user.notebook); }) .then(function(aNotebook) { notebook = aNotebook; return doSomethingAsync(user, notebook); }) .then(function(result) { response.send(result) }) .catch(function(err) { response.send(err) }) }

Finally, the await based version of the same code. No awkward functions, clear linear style code, explicit asynchronous points, and native try/catch error handling.

The simplest explanation of how this works is that await takes a promise, waits for it's value to be available, and then returns that value.

async function x(request, response) { try { var user = await User.get(request.user); var notebook = await Notebook.get(user.notebook); response.send(await doSomethingAsync(user, notebook)); } catch(err) { response.send(err); } }

This code returns a promise (represented by a Request object) if you run it.

require("popsicle").get("http://www.google.com");

Whereas, this code returns the value of that promise (the Response object) once it's ready.

await require("popsicle").get("http://www.google.com");

And of course, if you await a non promise, things work just fine.

await "Hello!"

There's one more slight complication. This function will produce a syntax error. That's because you can only use await inside of a function declared a special way.

function x(aPromise) { await aPromise }

Here's the corrected version of this simple function. We add the async keyword before our function declaration. Following the same rule, you can't have a top level await. Tonic, the program being used here, relaxes this rule since its so convenient when working in a REPL.

What does async do? Essentially, it wraps the return value of the function in a promise. Under the hood it is more or less using generators, but all of that is hidden away behind this simple syntax..

async function x(aPromise) { await aPromise }

"await" is not threads

In a language with threads, concurrency can be out of your control. Your thread can be interrupted at any time, so we need locks and other synchronization primitives.

But JavaScript's model hasn't changed, it's still single threaded. The code is only interrupted by your explicit command to "await". You can still mess up by sharing state, but that's nothing new.

await gives you explicit control over concurrency. You can combine this with powerful promise utilities like Promise.all, which will wait for every promise to finish and then finish itself, to write powerful and yet easy to understand asynchronous code.

var P = require("popsicle"); var sites = await Promise.all([ P.get("http://www.google.com"), P.get("http://www.apple.com"), P.get("http://www.yahoo.com") ])

You can use native promises, which are increasingly available, or a library like Bluebird that has an extensive set of utilities that make many tasks simple.

And npm is full of libraries that support promises, and promisified wrappers of libraries that don't yet have native support.

How can you use this right now? Babel.

If you aren't already using Babel, it's easy to install. And await is supported out of the box in the latest version!

UPDATE: Now available natively in Node v7.6.0!

Thanks!

@bouchertonicdev.com

Links