At some point you've written JavaScript code with callbacks, nested within callbacks, nested within those callbacks, etc... commonly referred to as callback hell. For instance, assume you were using the GitHub API to write a simple function that does the following:

  1. Get the repository's README in Markdown
  2. Convert the Markdown to HTML
  3. Save the HTML in a file

Since each of the above steps involves some sort of asynchronous I/O operation due to the API calls and file writing, we would get at least 3 levels of nested callbacks:

function getReadmeInHtml (repo) {
  getReadme(repo, (err, readme) => {
    convertMdToHtml(readme, (err, html) => {
      saveHtml(html, (err, filename) => {
        // HTML file is now saved
      })
    })
  })
}

Although your eyes may have adjusted to seeing this pattern over the years, it's no secret that it's difficult to reason about and maintain, especially for newcomers. Coming from languages like Python, Java, etc... you might miss how little thought you gave to using the return statement and error handling with try / catch blocks.

Thankfully, the introduction of Promises in ES2015 (ES6) gives us a well-defined contract for handling the results of an asynchronous operation. As a result, we can use Promises as the building blocks for our async functions.

If you're not familiar with how promises work, I'd encourage you to have a quick look at JavaScript Promises: Plain and Simple before reading on since they will be referred to frequently.

Async Functions

You might be wondering "how are Promises and async functions are related?" To answer this, let's start off by rewriting the above getReadmeInHtml (repo) function using the async / await syntax:

async function getReadmeInHtml (repo) {
  const readme = await getReadme(repo)
  const html = await convertMdToHtml(readme)
  const filename = await saveHtml(html)
}

As you can see, the async / await version looks pretty much like any synchronous code you would write. However there are a few things to note:

  1. Any function that you want to use the await keyword in must be prefixed with async
  2. The await keyword causes the current function (getReadmeInHtml) to stop executing until the Promise returned from the await-ed function (getReadme, convertMdToHtml, saveHtml) resolves before moving on to executing the next statement

The second point naturally brings us to the answer of how Promises and async functions are related. For the above code to work as expected, any function we await must return a Promise.

In the example above, the getReadme(repo) function we await must return a Promise. Once the Promise is resolved, the value it resolves to is assigned to the variable readme and the next statement is executed and await-ed, in this case: convertMdToHtml (readme), and so on... Just like synchronous code!

Let's take a look at what getReadme (repo) would actually look like if we implemented it using Promises:

function getReadme (repo) {
  let requestUrl = `https://api.github.com/repos/${repo}/readme`

  // fetch returns a Promise
  return fetch(requestUrl, {method: 'GET'}).then((res) => {
    // get the response in JSON format
    return res.json()
  }).then((data) => {
    // decode the base64 string returned by GitHub's API
    return atob(data.content)
  })
}

Since fetch returns a Promise, we can use the await keyword in our async function, getReadmeInHtml, to suspend execution in that context until the I/O request in getReadme has completed.

Again, the important point to remember is that any function you wish to use the await keyword with must return a Promise.

Async Functions Return Promises

A point that is not immediately obvious about async functions is that they always return a Promise. For instance, an async function that returns a value is actually returning a Promise which resolves to that value. The following are equivalent:

async function greet (name) {
  return 'hello ' + name
}

and

async function greet (name) {
  return Promise.resolve('hello ' + name)
}

This is because the async keyword will result in the return value being wrapped in a Promise object implicitly.

If you want to reject the Promise you can throw an error or explicitly reject a Promise:

async function greet (name) {
  if (name)
    return 'hello ' + name
  throw new Error('A name must be provided')
}

is the same as:

async function greet (name) {
  if (name)
    return Promise.resolve('hello ' + name)
  return Promise.reject('A name must be provided')
}

The key point from this section is that: async functions always return a Promise.

Error Handling in Async Functions

One of the great things about async functions is that you can use the already familiar try / catch construct for handling errors thrown by an asynchronous operation. You no longer need to redundantly check for errors, like so:

asyncOperation1((err, res) => {
  if (err) console.log(err)
  asyncOperation2((err, res) => {
    if (err) console.log(err)
  })
})

Instead, you can wrap await statements in try / catch blocks to capture and handle the errors from the await-ed promises just as you would with synchronous code:

async function getReadmeInHtml (repo) {
  try {
    const readme = await getReadme(repo)
    const html = await convertMdToHtml(readme)
    const filename = await saveHtml(html)
  } catch (err) {
    console.log('An error has occurred: ', err)
  }
}

It's that simple! If any of the await-ed functions results in a rejected Promise, the error will be caught and handled accordingly.

It's worth noting that if a function returning a Promise is rejected, the error will be swallowed silently in some browsers/environments. For this reason, it's best to wrap await-ed functions in try / catch blocks to avoid having errors go unhandled.

Async Functions and Loops

You might find yourself commonly using asynchronous functions in loops. For example, assume you wanted process a list of READMEs for GitHub repos in the order they are provided:

  1. Read a file containing a list of GitHub repos
  2. For each repo call getReadmeInHtml

With async functions, we can easily achieve this:

async function processReadmesFromFile (filename) {
  // read the list of repos from the file into an array
  const repos = await readRepos(filename)

  // we can use the for..of syntax to loop through the array of repositories
  for (let repo of repos) {
    // wait until the README is done processing before going onto the next
    await getReadmeInHtml(repo)
  }
}

Now each repository's README is grabbed, converted, and saved in the order specified in the file. We are able to perform these steps sequentially thanks to the await getReadmeInHtml(repo) statement, which waits until a repository's README is processed before going onto the next iteration of the for loop and processing the next README.

This is because any async function always returns a Promise as we mentioned earlier. Therefore the await clause in the example above causes the loop to suspend until the Promise returned from the getReadmeInHtml function resolves.

Concurrency with Promise.all

Let's revisit the example in the previous section where we read a file and processed the READMEs one at a time (sequentially). You may not care about the order in which the READMEs are processed and instead you want to process them as quickly as possible.

To avoid wasting time by waiting for each I/O operation to complete, we can use Promise.all to execute these requests concurrently and rewrite the example in the previous example to look like this:

async function processReadmesFromFile (filename) {
  // read the list of repos from the file into an array
  const repos = await readRepos(filename)

  // use `Promise.all` to wait until all promises have been resolved
  await Promise.all(repos.map((repo) => getReadmeInHtml(repo)))

  // all the READMEs have been processed at this point, you can access
  // the files or do whatever you need here
}

Let's take a second to break down the following statement from the example above:

await Promise.all(repos.map((repo) => getReadmeInHtml(repo)))

First we are using map method to create a new array of Promises returned by the getReadmeInHtml function. We then feed this array of promises to the Promise.all method which returns a Promise that resolves when all the Promises in the array have resolved.

Since Promise.all itself returns a Promise, we can use the await keyword to wait until all the promises have resolved before moving on to executing the next statement or returning from the function.

If it's easier to read, we can break down that statement into 2 lines, like so:

await Promise.all(repos.map((repo) => getReadmeInHtml(repo)))

// is the same as:

// create an array of promises by calling `getReadmeInHtml` on each of the repos
let promises = repos.map((repo) => getReadmeInHtml(repo))

// use Promise.all to let us know when ALL the promises have resolved
await Promise.all(promises)

Using Async Functions Today

There are many ways you can start using async functions in your applications and libraries today:

I'll show you a quick and easy way to use babel-cli and the transform-async-to-generator plugin to transpile your code which uses async functions into generators - an ES6 feature implemented in Node v6 and most modern browsers.

First, in an empty directory, initialize a package.json with the following command:

mkdir node-async && cd node-async
npm init -y

Next we need to install babel-cli and babel-plugin-transform-async-to-generator:

npm install --save-dev babel-cli babel-plugin-transform-async-to-generator

Once the dependencies have been installed, we need to configure Babel to use the plugin we just installed. You can do that through a .babelrc file or directly in the package.json, like so:

{
  "name": "node-async",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "babel-node index.js",
    "build": "babel index.js --out-file build.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-cli": "^6.11.4",
    "babel-plugin-transform-async-to-generator": "^6.8.0"
  },
  "babel": {
    "plugins": [
      "transform-async-to-generator"
    ]
  }
}

You will also notice 2 scripts defined:

  • npm run start will automatically compile and run your Node.js code using babel-node, which is handy when developing
  • npm run build will output the compiled code to a file called build.js, ready to be run using node build.js

And that's it! Try it out for yourself by creating a simple async function and saving it in index.js:

async function test () {
  return 'Testing async functions'
}

test().then(val => console.log(val))

then running npm run start.