JavaScript
Article
By Joe Zimmerman , Nilson Jacques

Simplifying Asynchronous Coding with Async Functions

By Joe Zimmerman , Nilson Jacques

An hourglass with sand pouring through

The debut of Promises in JavaScript has lit the internet on fire—they help developers break out of callback hell and solve a lot of problems that have plagued the asynchronous code of JavaScript programmers everywhere. Promises are far from flawless, though. They still require callbacks, can still be messy in complex situations, and are incredibly verbose.

22nd March 2017: This article has been updated to reflect changes to the specification, and current runtime support.

With the advent of ES6 (referred to as ES2015 from here on), which not only made promises native to the language without requiring one of the countless available libraries, we also got generators. Generators have the ability to pause execution within a function, which means that by wrapping them in a utility function, we have the ability to wait for an asynchronous operation to finish before moving on to the next line of code. Suddenly your asynchronous code could start to look synchronous!

But that was just the first step. Async functions are due to be standardized as part of the ES2017 specification this year and native support is already growing. Async functions take the idea of using generators for asynchronous programming and give them their own simple and semantic syntax. Consequently, you don’t have to use a library to get that wrapping utility function, because that is handled in the background.

To run the async/await code samples from this article, you’ll need a compatible browser.

Runtime Compatibility

On the client-side, Chrome, Firefox and Opera now support async functions out of the box.

Can I Use async-functions? Data on support for the async-functions feature across the major browsers from caniuse.com.

As of version 7.6, Node.js also ships with async/await enabled by default.

Async Functions vs Generators

Here is an example of using generators for asynchronous programming. It uses the Q library:

var doAsyncOp = Q.async(function* () {
  var val = yield asynchronousOperation();
  console.log(val);
  return val;
});

Q.async is the wrapper function that handles everything behind the scenes. The * is what denotes the function as a generator function and yield is how you pause the function and let the wrapper function take over. Q.async will return a function that you can assign—as I have done—to doAsyncOp and subsequently invoke.

Here’s what it looks like when you get rid of the cruft by using the new syntax included in ES7:

async function doAsyncOp () {
  var val = await asynchronousOperation();     
  console.log(val);
  return val;
};

It’s not a lot different, but we removed the wrapper function and the asterisk and replaced them with the async keyword. The yield keyword was also replaced by await. These two examples will do the exactly same thing: wait for asynchronousOperation to complete before assigning its value to val, logging it, and returning it.

Converting Promises to Async Functions

What would the previous example look like if we were using vanilla promises?

function doAsyncOp () {
  return asynchronousOperation().then(function(val) {
    console.log(val);
    return val;
  });
};

This has the same number of lines, but there is plenty of extra code due to then and the callback function passed to it. The other nuisance is the duplication of the return keyword. This has always been something that bugged me because it makes it difficult to figure out exactly what is being returned from a function that uses promises.

As you can see, this function returns a promise that will fulfill to the value of val. And guess what … so do the generator and async function examples! Whenever you return a value from one of those functions, you are actually implicitly returning a promise that resolves to that value. If you don’t return anything at all, you are implicitly returning a promise that resolves to undefined.

Chaining Operations

One of the aspects of promises that hooks many people is the ability to chain multiple asynchronous operations without running into nested callbacks. This is one of the areas in which async functions excel even more than promises.

This is how you would chain asynchronous operations using promises (admittedly we’re being silly and just running the same asynchronousOperation over and over again).

function doAsyncOp() {
  return asynchronousOperation()
    .then(function(val) {
      return asynchronousOperation(val);
    })
    .then(function(val) {
      return asynchronousOperation(val);
    })
    .then(function(val) {
      return asynchronousOperation(val);
    });
}

With async functions, we can just act like asynchronousOperation is synchronous:

async function doAsyncOp () {
  var val = await asynchronousOperation();
  val = await asynchronousOperation(val);
  val = await asynchronousOperation(val);
  return await asynchronousOperation(val);
};

You don’t even need the await keyword on that return statement because either way, it will return a promise resolving to the final value.

--ADVERTISEMENT--

Parallel Operations

One of the other great features of promises is the ability to run multiple asynchronous operations at once and continue on your way once all of them have completed. Promise.all() is the way to do this according to the ES2015 spec.

Here’s an example:

function doAsyncOp() {
  return Promise.all([
    asynchronousOperation(),
    asynchronousOperation()
  ]).then(function(vals) {
    vals.forEach(console.log);
    return vals;
  });
}

This is also possible with async functions, though you still need to use Promise.all():

async function doAsyncOp() {
  var vals = await Promise.all([
    asynchronousOperation(),
    asynchronousOperation()
  ]);
  vals.forEach(console.log.bind(console));
  return vals;
}

It’s still much cleaner, even with the Promise.all bit in there.

Handling Rejection

Promises have the ability to be resolved or rejected. Rejected promises can be handled with the second function passed to then or with the catch method. Since we’re not using any Promise API methods, how would we handle a rejection? We do it with a try and catch. When using async functions, rejections are passed around as errors and this allows them to be handled with built-in JavaScript error handling code.

function doAsyncOp() {
  return asynchronousOperation()
    .then(function(val) {
      return asynchronousOperation(val);
    })
    .then(function(val) {
      return asynchronousOperation(val);
    })
    .catch(function(err) {
      console.error(err);
    });
}

That’s pretty similar to our chaining example except we replaced the final chained call with a catch. Here’s what it would look like with async functions.

async function doAsyncOp () {
  try {
    var val = await asynchronousOperation();
    val = await asynchronousOperation(val);
    return await asynchronousOperation(val);
  } catch (err) {
    console.err(err);
  }
};

It’s not as terse as the other conversions to async functions, but it is exactly how you would do it with synchronous code. If you don’t catch the error here, it’ll bubble up until it is caught in the caller functions, or it will just not be caught and you’ll kill execution with a run-time error. Promises work the same way, except that rejections don’t need to be errors; they can just be a string explaining what went wrong. If you don’t catch a rejection that was created with an error, then you will see a run-time error, but if you just use a string, then it will fail silently.

Broken Promises

To reject a native promise you can use reject inside the Promise constructor, or you can throw an error—either inside the Promise constructor or within a then or catch callback. If an error is thrown outside of that scope, it won’t be contained in the promise.

Here are some examples of ways to reject promises:

function doAsyncOp() {
  return new Promise(function(resolve, reject) {
    if (somethingIsBad) {
      reject("something is bad");
    }
    resolve("nothing is bad");
  });
}

/*-- or --*/

function doAsyncOp() {
  return new Promise(function(resolve, reject) {
    if (somethingIsBad) {
      reject(new Error("something is bad"));
    }
    resolve("nothing is bad");
  });
}

/*-- or --*/

function doAsyncOp() {
  return new Promise(function(resolve, reject) {
    if (somethingIsBad) {
      throw new Error("something is bad");
    }
    resolve("nothing is bad");
  });
}

Generally, it is best to use the new Error whenever you can because it will contain additional information about the error, such as the line number where it was thrown, and a potentially useful stack trace.

Here are some examples where throwing an error will not be caught by the promise:

function doAsyncOp() {
  // the next line will kill execution
  throw new Error("something is bad");
  return new Promise(function(resolve, reject) {
    if (somethingIsBad) {
      throw new Error("something is bad");
    }
    resolve("nothing is bad");
  });
}

// assume `doAsyncOp` does not have the killing error
function x() {
  var val = doAsyncOp().then(function() {
    // this one will work just fine
    throw new Error("I just think an error should be here");
  });
  // this one will kill execution
  throw new Error("The more errors, the merrier");
  return val;
}

With async functions promises are rejected by throwing errors. The scope issue doesn’t arise—you can throw an error anywhere within an async function and it will be caught by the promise:

async function doAsyncOp() {
  // the next line is fine
  throw new Error("something is bad");
  if (somethingIsBad) {
    // this one is good too
    throw new Error("something is bad");
  }
  return "nothing is bad";
} 

// assume `doAsyncOp` does not have the killing error
async function x() {
  var val = await doAsyncOp();
  // this one will work just fine
  throw new Error("I just think an error should be here");
  return val;
}

Of course, we’ll never get to that second error or to the return inside the doAsyncOp function because the error will be thrown and will stop execution within that function.

Gotchas

If you’re new to async functions, one gotcha to be aware of is using nested functions. For example, if you have another function within your async function (generally as a callback to something), you may think that you can just use await from within that function. You can’t. You can only use await directly within an async function.

For example, this does not work:

async function getAllFiles(fileNames) {
  return Promise.all(
    fileNames.map(function(fileName) {
      var file = await getFileAsync(fileName);
      return parse(file);
    })
  );
}

The await on line 4 is invalid because it is used inside a normal function. Instead, the callback function must have the async keyword attached to it.

async function getAllFiles(fileNames) {
  return Promise.all(
    fileNames.map(async function(fileName) {
      var file = await getFileAsync(fileName);
      return parse(file);
    })
  );
}

It’s obvious when you see it, but nonetheless, it’s something that you need to watch out for.

In case you’re wondering, here’s the equivalent using promises:

function getAllFiles(fileNames) {
  return Promise.all(
    fileNames.map(function(fileName) {
      return getFileAsync(fileName).then(function(file) {
        return parse(file);
      });
    })
  );
}

The next gotcha relates to people thinking that async functions are synchronous functions. Remember, the code inside the async function will run as if it is synchronous, but it will still immediately return a promise and allow other code to execute outside of it while it works to fulfillment. For example:

var a = doAsyncOp(); // one of the working ones from earlier
console.log(a);
a.then(function() {
  console.log("`a` finished");
});
console.log("hello");

/* -- will output -- */
Promise Object
hello
`a` finished

You can see that async functions still utilize built-in promises, but they do so under the hood. This gives us the ability to think synchronously while within an async function, although others can invoke our async functions using the normal Promise API or using async functions of their own.

Better Async Code, Today!

Even if you can’t use it natively, you can write it and use tools to compile it down to ES5. Async functions are all about making your code more readable and therefore more maintainable. As long as we have source maps, we can always work with the cleaner ES2017 code.

There are several tools that can compile async functions (and other ES2015+ features) down to ES5 code. If you’re using Babel, it’s simply a case of installing the ES2017 preset.

Are you already taking advantage of the amazing power yielded to us through async functions? Is this something you’d consider using today? Let us know in the comments.

Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account