Arguably the biggest new feature in Node.js 7.6.0 is that the much awaited async function keyword is now available without a flag. Callback hell and promise hell are now in the past. But, like Uncle Ben always reminded us, with great power comes great responsibility, and async/await gives you a lot of new and exciting ways to shoot yourself in the foot. You still need to handle errors and be aware of the async nature of your code, otherwise you'll inevitably be complaining about "async/await hell" in 6 months.

All code in this article was tested on node.js 7.6.0. It will not work on earlier versions of node. Node.js 7.x is an odd-numbered Node.js release, which means it is scheduled to be deprecated in June 2017, so I wouldn't recommend using it in production.

Hello, World

Here's a "Hello, World" example of using async/await:

async function test() {
  await new Promise((resolve, reject) => setTimeout(() => resolve(), 1000));
  console.log('Hello, World!');
}

test();

You can run this node script like any other, without any transpilers, and it will print out "Hello, World!" after approximately 1 second. This is how you can implement JavaScript sleep with async/await.

$ ~/Workspace/node-v7.6.0-linux-x64/bin/node async.js
Hello, World!
$
$ time ~/Workspace/node-v7.6.0-linux-x64/bin/node async.js
Hello, World!

real    0m1.121s
user    0m0.115s
sys    0m0.008s
$

Async functions are based entirely on promises. You should always await on a promise. Using await on a non-promise won't do anything:

async function test() {
  // Works, just doesn't do anything useful
  await 5;
  console.log('Hello, World!');
}

test();

You don't need to use native Node.js promises with await. Bluebird or any other promise lib should work. In general, using await with any object that has a then() function will work:

async function test() {
  // This object is a "thenable". It's a promise by the letter of the law,
  // but not the spirit of the law.
  await { then: resolve => setTimeout(() => resolve(), 1000) };
  console.log('Hello, World!');
}

test();

There is one major restriction for using await: you must use await within a function that's declared async. The below code will result in a SyntaxError.

function test() {
  const p = new Promise(resolve => setTimeout(() => resolve(), 1000));
  // SyntaxError: Unexpected identifier
  await p;
}

test();

Furthermore, await must not be in a closure embedded in an async function, unless the closure is also an async function. The below code also results in a SyntaxError:

const assert = require('assert');

async function test() {
  const p = Promise.resolve('test');
  assert.doesNotThrow(function() {
    // SyntaxError: Unexpected identifier
    await p;
  });
  console.log('Hello, world!');
}

test();

Another important detail to remember about async functions is that async functions return promises:

async function test() {
  await new Promise((resolve, reject) => setTimeout(() => resolve(), 1000));
  console.log('Hello, World!');
}

// Prints "Promise { <pending> }"
console.log(test());

This means that you can await on the result of an async function.

async function wait(ms) {
  await new Promise(resolve => setTimeout(() => resolve(), ms));
}

async function test() {
  // Since `wait()` is marked `async`, the return value is a promise, so
  // you can `await`
  await wait(1000);
  console.log('Hello, World!');
}

test();

Return Values and Exceptions

A promise can either resolve to a value or reject with an error. Async/await lets you handle these cases with synchronous operators: assignment for resolved values, and try/catch for exceptions. The return value of await is the value that the promise resolves to:

async function test() {
  const res = await new Promise(resolve => {
    // This promise resolves to "Hello, World!" after ~ 1sec
    setTimeout(() => resolve('Hello, World!'), 1000);
  });
  // Prints "Hello, World!". `res` is equal to the value the promise resolved to
  console.log(res);
}

test();

In an async function, you can use try/catch to catch promise rejections. In other words, asynchronous promise rejections behave like synchronous errors:

async function test() {
  try {
    await new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error('Woops!')), 1000);
    });
  } catch (error) {
    // Prints "Caught Woops!"
    console.log('Caught', error.message);
  }
}

test();

Using try/catch as an error handling mechanism is powerful because it enables you to handle synchronous errors and asynchronous errors with a single syntax. In callback land you'd often have to wrap your async calls in a try/catch as well as handling the error callback parameter.

function bad() {
  throw new Error('bad');
}

function bad2() {
  return new Promise(() => { throw new Error('bad2'); });
}

async function test() {
  try {
    await bad();
  } catch (error) {
    console.log('caught', error.message);
  }

  try {
    await bad2();
  } catch (error) {
    console.log('caught', error.message);
  }
}

test();

Loops and Conditionals

The number one killer feature of async/await is that you can write async code using if statements, for loops, and all the other synchronous constructs that you had to swear off of when using callbacks. You don't need any flow control libraries with async/await, you just use conditionals and loops. Here's an example using a for loop:

function wait(ms) {
  return new Promise(resolve => setTimeout(() => resolve(), ms));
}

async function test() {
  for (let i = 0; i < 10; ++i) {
    await wait(1000);
    // Prints out "Hello, World!" once per second and then exits
    console.log('Hello, World!');
  }
}

test();

And an example using an if statement:

function wait(ms) {
  return new Promise(resolve => setTimeout(() => resolve(), ms));
}

async function test() {
  for (let i = 0; i < 10; ++i) {
    if (i < 5) {
      await wait(1000);
    }
    // Prints out "Hello, World!" once per second 5 times, then prints it 5 times immediately
    console.log('Hello, World!');
  }
}

test();

Remember It's Asynchronous

One cute JavaScript interview question I used to ask was what would the below code print out?

for (var i = 0; i < 5; ++i) {
  // Actually prints out "5" 5 times.
  // But if you use `let` above, it'll print out 0-4
  setTimeout(() => console.log(i), 0);
}

// This will print *before* the 5's
console.log('end');

Asynchronous code is confusing, and async/await makes it easier to write asyncronous code but doesn't change the nature of asynchronous code. Just because async functions look synchronous doesn't mean they are:

function wait(ms) {
  return new Promise(resolve => setTimeout(() => resolve(), ms));
}

async function test(ms) {
  for (let i = 0; i < 5; ++i) {
    await wait(ms);
    console.log(ms * (i + 1));
  }
}

// These two function calls will actually run in parallel
test(70);
test(130);

// Output
70
130
140
210
260
280
350
390
520
650

Error Handling

Remember that you can only use await within an async function, and async functions return promises. This means that somewhere in your code you're going to have to handle errors. Async/await gives you a powerful mechanism for aggregating errors: all errors that occur in the async function, synchronous or asynchronous, are bubbled up as a promise rejection. However, it's up to you to handle the error. Here's another interesting article on the topic of handling promise rejections with async/await.

Let's say you want to use async/await with the Express web framework. The naive way of using async functions with Express works in the most basic case:

const express = require('express');

const app = express();

app.get('/', handler);

app.listen(3000);

async function handler(req, res) {
  // Will wait approximately 1 second before sending the result
  await wait(1000);
  res.send('Hello, world');
}

function wait(ms) {
  return new Promise(resolve => setTimeout(() => resolve(), ms));
}

So we're done, right? Wrong. What happens if you throw an exception in the handler function?

const express = require('express');

const app = express();

app.get('/', handler);

app.listen(3000);

async function handler(req, res) {
  throw new Error('Hang!');
}

function wait(ms) {
  return new Promise(resolve => setTimeout(() => resolve(), ms));
}

Express will hang forever, the server won't crash, and the only indication of an error would be an unhandled promise rejection warning.

$ node async.js
(node:17661) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Hang!
(node:17661) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Since await treats promise rejections as exceptions, unless you try/catch around await the rejection will cause the entire function to stop executing. The below handler function will also hang with an unhandled promise rejection warning.

async function handler(req, res) {
  await new Promise((resolve, reject) => reject(new Error('Hang!')));
  res.send('Hello, World!');
}

The single most important takeaway from this article is that async functions return a promise. Async/await gives you the ability to build sophisticated async logic with loops, conditionals, and try/catch, but in the end it packages all this logic into a single promise. If you see async/await code that doesn't include any .catch() calls, odds are that code is missing some error cases. Here's a better way of handling async functions with Express:

const express = require('express');

const app = express();

app.get('/', safeHandler(handler));

app.listen(3000);

function safeHandler(handler) {
  return function(req, res) {
    handler(req, res).catch(error => res.status(500).send(error.message));
  };
}

async function handler(req, res) {
  await new Promise((resolve, reject) => reject(new Error('Hang!')));
  res.send('Hello, World!');
}

The safeHandler function chains .catch() onto the promise that the async handler function returns. This ensures that your server sends an HTTP response, even if handler errors out. If calling safeHandler on every request handler seems cumbersome, there are numerous alternatives, like using observables or ramda.

Async/Await vs Co/Yield

The co library uses ES6 generators to get functionality similar to async/await. For example, here's how the safeHandler code example would look with co/yield:

const co = require('co');
const express = require('express');

const app = express();

app.get('/', safeHandler(handler));

app.listen(3000);

function safeHandler(handler) {
  return function(req, res) {
    handler(req, res).catch(error => res.status(500).send(error.message));
  };
}

function handler(req, res) {
  return co(function*() {
    yield new Promise((resolve, reject) => reject(new Error('Hang!')));
    res.send('Hello, World!');
  });
}

As a matter of fact, you can replace every async function(params) {} in this article with function(params) { return co(function*() {}) } and await with yield and all the examples will still work.

The upside of using co is that co works without any transpilation in Node.js 4.x and 6.x. The EOL of 4.x and 6.x is in 2018 and 2019, respectively, so these releases are more stable than Node.js 7.x. Until Node.js 8 is released (tentatively scheduled for April 2017) there's no LTS version of Node.js that supports async/await without transpilers. Co also enjoys better browser support, and every transpiler for async/await that I know of compiles down to using generators. If you're interested in really mastering co and generators, check out my ebook, The 80/20 Guide to ES2015 Generators, which walks you through writing your own co from scratch.

Async/await has numerous advantages, most notably readable stack traces. Let's compare the stack traces of using co vs using async/await with the Express example:

function handler(req, res) {
  return co(function*() {
    yield new Promise((resolve, reject) => reject(new Error('Hang!')));
    res.send('Hello, World!');
  });
}

// --- versus ---

async function handler(req, res) {
  await new Promise((resolve, reject) => reject(new Error('Hang!')));
  res.send('Hello, World!');
}

Async:

$ node async.js
Error: Hang!
    at Promise (/home/val/async.js:16:49)
    at handler (/home/val/async.js:16:9)
    at /home/val/async.js:11:5
    at Layer.handle [as handle_request] (/home/val/node_modules/express/lib/router/layer.js:95:5)
    at next (/home/val/node_modules/express/lib/router/route.js:137:13)
    at Route.dispatch (/home/val/node_modules/express/lib/router/route.js:112:3)
    at Layer.handle [as handle_request] (/home/val/node_modules/express/lib/router/layer.js:95:5)
    at /home/val/node_modules/express/lib/router/index.js:281:22
    at Function.process_params (/home/val/node_modules/express/lib/router/index.js:335:12)
    at next (/home/val/node_modules/express/lib/router/index.js:275:10)

Co:

$ node async.js
Error: Hang!
    at Promise (/home/val/async.js:18:51)
    at /home/val/async.js:18:11
    at Generator.next (<anonymous>)
    at onFulfilled (/home/val/node_modules/co/index.js:65:19)
    at /home/val/node_modules/co/index.js:54:5
    at co (/home/val/node_modules/co/index.js:50:10)
    at handler (/home/val/async.js:17:10)
    at /home/val/async.js:12:5
    at Layer.handle [as handle_request] (/home/val/node_modules/express/lib/router/layer.js:95:5)
    at next (/home/val/node_modules/express/lib/router/route.js:137:13)

So async/await has better stack traces and lets you construct promises using the built-in loops and conditionals that you already know, so download Node.js 7.6 and give it a shot!

Looking to become fluent in async/await? My new ebook, Mastering Async/Await, is designed to give you an integrated understanding of async/await fundamentals and how async/await fits in the JavaScript ecosystem in a few hours. Get your copy!

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus