Async functions: making promises friendly
Stay organized with collections
Save and categorize content based on your preferences.
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:
asyncfunctionmyFirstAsyncFunction(){
try{
constfulfilledValue=awaitpromise;
}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:
functionlogFetch(url){
returnfetch(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:
asyncfunctionlogFetch(url){
try{
constresponse=awaitfetch(url);
console.log(awaitresponse.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
functionwait(ms){
returnnewPromise((r)=>setTimeout(r,ms));
}
asyncfunctionhello(){
awaitwait(500);
return'world';
}
...calling hello() returns a promise that fulfills with "world".
asyncfunctionfoo(){
awaitwait(500);
throwError('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:
functiongetResponseSize(url){
returnfetch(url).then((response)=>{
constreader=response.body.getReader();
lettotal=0;
returnreader.read().then(functionprocessResult(result){
if(result.done)returntotal;
constvalue=result.value;
total+=value.length;
console.log('Received chunk',value);
returnreader.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:
asyncfunctiongetResponseSize(url){
constresponse=awaitfetch(url);
constreader=response.body.getReader();
letresult=awaitreader.read();
lettotal=0;
while(!result.done){
constvalue=result.value;
total+=value.length;
console.log('Received chunk',value);
// get the next result
result=awaitreader.read();
}
returntotal;
}
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
constjsonPromises=urls.map(async(url)=>{
constresponse=awaitfetch(url);
returnresponse.json();
});
Object methods
conststorage={
asyncgetAvatar(name){
constcache=awaitcaches.open('avatars');
returncache.match(`/avatars/${name}.jpg`);
}
};
storage.getAvatar('jaffathecake').then(...);
Class methods
classStorage{
constructor(){
this.cachePromise=caches.open('avatars');
}
asyncgetAvatar(name){
constcache=awaitthis.cachePromise;
returncache.match(`/avatars/${name}.jpg`);
}
}
conststorage=newStorage();
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.
asyncfunctionseries(){
awaitwait(500);// Wait 500ms...
awaitwait(500);// ...then wait another 500ms.
return'done!';
}
The above takes 1000ms to complete, whereas:
asyncfunctionparallel(){
constwait1=wait(500);// Start a 500ms timer asynchronously...
constwait2=wait(500);// ...meaning this timer happens in parallel.
awaitPromise.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:
functionmarkHandled(promise){
promise.catch(()=>{});
returnpromise;
}
functionlogInOrder(urls){
// fetch all the URLs
consttextPromises=urls.map((url)=>{
returnmarkHandled(fetch(url).then((response)=>response.text()));
});
// log them in order
returntextPromises.reduce((chain,textPromise)=>{
returnchain.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:
asyncfunctionlogInOrder(urls){ for(consturlofurls){ constresponse=awaitfetch(url); console.log(awaitresponse.text()); } }
functionmarkHandled(...promises){ Promise.allSettled(promises); } asyncfunctionlogInOrder(urls){ // fetch all the URLs in parallel consttextPromises=urls.map(async(url)=>{ constresponse=awaitfetch(url); returnresponse.text(); }); markHandled(...textPromises); // log them in sequence for(consttextPromiseoftextPromises){ console.log(awaittextPromise); } }
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
- 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:
asyncfunctionslowEcho(val){
awaitwait(1000);
returnval;
}
...you'd include the polyfill and write:
constslowEcho=createAsyncFunction(function*(val){
yieldwait(1000);
returnval;
});
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!