Introduction
A function defined as async
is a function that can perform asynchronous actions but still look synchronous. The way it's done is using the await
keyword to pause the function while it waits for a Promise to resolve/reject.
Note: Async functions are a stage 3 proposal and are not yet integrated into the ECMAScript standard.
async function getJSON(url) {
const response = await fetch(url);
return response.json();
}
An async function always returns a Promise itself, so you can use it in other asynchronous functions.
Await and operator precedence
You have to keep the operator precedence in mind when using await
keyword.
Imagine that we have an asynchronous function getUnicorn()
which returns a Promise that resolves to an instance of class Unicorn
. Now we want to get the size of the unicorn using the getSize()
method of that class.
Look at the following code:
await getUnicorn().getSize()
At first sight it looks valid, but actually it's not. Due to operator precedence, it's equivalent to the following:
await (getUnicorn().getSize())
Here we attempt to call getSize()
method of the Promise object, which isn't what we want.
Instead, we should use brackets to denote that we first want to wait for the unicorn, and then call getSize()
method of the result:
(await getUnicorn()).getSize()
Of course the previous version could be valid in some cases, for example if the getUnicorn()
function was synchronous, but the getSize()
method was asynchronous.
Async functions compared to Promises
Consider following function using promises:
function newUnicorn() {
return fetch('unicorn.json') // fetch unicorn.json from server
.then(response => response.json()) // parse the response as JSON
.then(unicorn =>
fetch('new/unicorn', { // send a request to 'new/unicorn'
method: 'post', // using POST method
body: JSON.stringify({unicorn}) // pass the unicorn to the request body
})
)
.then(response => response.json())
.then(response => response.success) // return success property of response
.catch(err => console.log('Error creating unicorn: ' + err));
}
It can be rewritten using async / await like that:
async function newUnicorn() {
try {
const unicorn = (await fetch('unicorn.json')).json()
const response = (
await fetch('new/unicorn', {
method: 'post',
body: JSON.stringify({unicorn})
})
).json()
return response.success
} catch (err) {
console.log('Error creating unicorn: ' + err);
}
}
Basically, placing the await
keyword allows you to use asynchronous functions (that is, functions that returns promises) as if they were synchronous. Note that you can only use await
in async
functions. You can also use async IIFE if you want to execute that code immediately:
(async () => {
await makeCoffee()
console.log('coffee is ready!')
})()
Looping with async await
When using async await in loops, you might encounter some of these problems.
If you just try to use await inside forEach
, this will throw an Unexpected token
error.
(async() => {
data = [1, 2, 3, 4, 5];
data.forEach(e => {
const i = await somePromiseFn(e);
console.log(i);
});
})();
This comes from the fact that you've erroneously seen the arrow function as a block. The await
will be in the context of the callback function, which is not async
.
The interpreter protects us from making the above error, but if you add async
to the forEach
callback no errors get thrown. You might think this solves the problem, but it won't work as expected.
Example:
(async() => {
data = [1, 2, 3, 4, 5];
data.forEach(async(e) => {
const i = await somePromiseFn(e);
console.log(i);
});
console.log('this will print first');
})();
This happens because the callback async function can only pause itself, not the parent async function.
You could write an asyncForEach function that returns a promise and then you could something like
await asyncForEach(async (e) => await somePromiseFn(e), data )
Basically you return a promise that resolves when all the callbacks are awaited and done. But there are better ways of doing this, and that is to just use a loop.
You can use a for-of
loop or a for/while
loop, it doesn't really matter which one you pick.
(async() => {
data = [1, 2, 3, 4, 5];
for (let e of data) {
const i = await somePromiseFn(e);
console.log(i);
}
console.log('this will print last');
})();
But there's another catch. This solution will wait for each call to somePromiseFn
to complete before iterating over the next one.
This is great if you actually want your somePromiseFn
invocations to be executed in order but if you want them to run concurrently, you will need to await
on Promise.all
.
(async() => {
data = [1, 2, 3, 4, 5];
const p = await Promise.all(data.map(async(e) => await somePromiseFn(e)));
console.log(...p);
})();
Promise.all
receives an array of promises as its only parameter and returns a promise. When all of the promises in the array are resolved, the returned promise is also resolved. We await
on that promise and when it's resolved all our values are available.
The above examples are fully runnable. The somePromiseFn
function can be made as an async echo function with a timeout.
You can try out the examples in the babel-repl with at least the stage-3
preset and look at the output.
function somePromiseFn(n) {
return new Promise((res, rej) => {
setTimeout(() => res(n), 250);
});
}
Less indentation
With promises:
function doTheThing() {
return doOneThing()
.then(doAnother)
.then(doSomeMore)
.catch(handleErrors)
}
With async functions:
async function doTheThing() {
try {
const one = await doOneThing();
const another = await doAnother(one);
return await doSomeMore(another);
} catch (err) {
handleErrors(err);
}
}
Note how the return is at the bottom, and not at the top, and you use the language's native error handling mechanics (try/catch
)