Async functions: making promises friendly

Async functions allow you to write promise-based code as if it were synchronous.

Jake Archibald
Jake Archibald

Async functions are enabled by default in Chrome, Edge, Firefox, and Safari, and they're quite frankly marvelous. They allow you to write promise-based code as if it were synchronous, but without blocking the main thread. They make your asynchronous code less "clever" and more readable.

Async functions work like this:

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

If you use the async keyword before a function definition, you can then use await within the function. When you await a promise, the function is paused in a non-blocking way until the promise settles. If the promise fulfills, you get the value back. If the promise rejects, the rejected value is thrown.

Browser support

Browser Support

  • Chrome: 55.
  • Edge: 15.
  • Firefox: 52.
  • Safari: 10.1.

Source

Example: logging a fetch

Say you want to fetch a URL and log the response as text. Here's how it looks using promises:

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

And here's the same thing using async functions:

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

It's the same number of lines, but all the callbacks are gone. This makes it way easier to read, especially for those less familiar with promises.

Async return values

Async functions always return a promise, whether you use await or not. That promise resolves with whatever the async function returns, or rejects with whatever the async function throws. So with:

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

…calling hello() returns a promise that fulfills with "world".

async function foo() {
  await wait(500);
  throw Error('bar');
}

…calling foo() returns a promise that rejects with Error('bar').

Example: streaming a response

The benefit of async functions increases in more complex examples. Say you wanted to stream a response while logging out the chunks, and return the final size.

Here it is with promises:

function getResponseSize(url) {
  return fetch(url).then((response) => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    });
  });
}

Check me out, Jake "wielder of promises" Archibald. See how I'm calling processResult() inside itself to set up an asynchronous loop? Writing that made me feel very smart. But like most "smart" code, you have to stare at it for ages to figure out what it's doing, like one of those magic-eye pictures from the 90's.

Let's try that again with async functions:

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

All the "smart" is gone. The asynchronous loop that made me feel so smug is replaced with a trusty, boring, while-loop. Much better. In future, you'll get async iterators, which would replace the while loop with a for-of loop, making it even neater.

Other async function syntax

I've shown you async function() {} already, but the async keyword can be used with other function syntax:

Arrow functions

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

Object methods

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then();

Class methods

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then();

Careful! Avoid going too sequential

Although you're writing code that looks synchronous, ensure you don't miss the opportunity to do things in parallel.

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

The above takes 1000ms to complete, whereas:

async function parallel() {
  const wait1 = wait(500); // Start a 500ms timer asynchronously…
  const wait2 = wait(500); // …meaning this timer happens in parallel.
  await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
  return 'done!';
}

The above takes 500ms to complete, because both waits happen at the same time. Let's look at a practical example.

Example: outputting fetches in order

Say you wanted to fetch a series of URLs and log them as soon as possible, in the correct order.

Deep breath - here's how that looks with promises:

function markHandled(promise) {
  promise.catch(() => {});
  return promise;
}

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map((url) => {
    return markHandled(fetch(url).then((response) => response.text()));
  });

  // log them in order
  return textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
  }, Promise.resolve());
}

Yeah, that's right, I'm using reduce to chain a sequence of promises. I'm so smart. But this is a bit of so smart coding you're better off without.

However, when converting the above to an async function, it's tempting to go too sequential:

Not recommended - too sequential
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Looks much neater, but my second fetch doesn't begin until my first fetch has been fully read, and so on. This is much slower than the promises example that performs the fetches in parallel. Thankfully there's an ideal middle-ground.
Recommended - nice and parallel
function markHandled(...promises) {
  Promise.allSettled(promises);
}

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  markHandled(...textPromises);

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
In this example, the URLs are fetched and read in parallel, but the "smart" reduce bit is replaced with a standard, boring, readable for-loop.

Browser support workaround: generators

If you're targeting browsers that support generators (which includes the latest version of every major browser ) you can sort-of polyfill async functions.

Babel will do this for you, here's an example via the Babel REPL

I recommend the transpiling approach, because you can just turn it off once your target browsers support async functions, but if you really don't want to use a transpiler, you can take Babel's polyfill and use it yourself. Instead of:

async function slowEcho(val) {
  await wait(1000);
  return val;
}

…you'd include the polyfill and write:

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

Note that you have to pass a generator (function*) to createAsyncFunction, and use yield instead of await. Other than that it works the same.

Workaround: regenerator

If you're targeting older browsers, Babel can also transpile generators, allowing you to use async functions all the way down to IE8. To do this you need Babel's es2017 preset and the es2015 preset.

The output is not as pretty, so watch out for code-bloat.

Async all the things!

Once async functions land across all browsers, use them on every promise-returning function! Not only do they make your code tidier, but it makes sure that function will always return a promise.

I got really excited about async functions back in 2014, and it's great to see them land, for real, in browsers. Whoop!