Async functions allow you to write promise-based code as if it were synchronous.
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
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:
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
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);
}
}
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
- note how similar the transpiled code is. This transformation is part of Babel's es2017 preset.
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!