Async/Await
Allows you to write asynchronous code that appears synchronous.
Needs no introduction, because every JS developer knows the pain of trying to cleanly manage a series of async operations via callbacks alone.
Until Promises became mainstream, we would generally wrap callbacks in a Promise so that they would work with a Promise chain. Then we could manage the operations in a more flattened manner like so:
// Imagine this as any block of asynchronous code that does something with an optional parameter.
function someAsyncOperation(offset = 0) {
return new Promise(resolve => {
setTimeout(() => resolve(offset + Math.ceil(Math.random() * 100)), 1000)
})
}
// Three async operations, each one dependent on the result of the former
someAsyncOperation()
.then(first => {
return someAsyncOperation(first)
})
.then(second => {
return someAsyncOperation(second)
})
.then(third => {
console.log(third)
})
The await
operator allows you to stop the execution of the current function until a Promise finishes execution. Any function that uses the async
operator must mark itself as async
in the function definition so that any code that utilizes it knows that the function is asynchronous, and can choose how to handle that.
The former example could be rewritten as:
function someAsyncOperation(offset = 0) {
return new Promise(resolve => {
setTimeout(() => resolve(offset + Math.ceil(Math.random() * 100)), 1000)
})
}
async function main() {
const first = await someAsyncOperation()
const second = await someAsyncOperation(first)
const third = await someAsyncOperation(second)
console.log(third)
}
// 50/50 chance of this function being async or sync, but await can be used either way
function maybeAsyncOperation() {
if (Math.round(Math.random() * 2)) {
return 100;
} else {
return new Promise((resolve) => { /** some async operation */ })
}
}
async function main() {
const result = await maybeAsyncOperation()
console.log(result)
}
main()
Instead of .catch
as used in a Promise chain, you use the good old-fashioned try
/catch
with await
, and any rejected Promise will fall to the catch
block.
function mightThrowAnError() {
if (Math.round(Math.random() * 2)) {
throw new Error('The ship is down!')
} else {
return new Promise((resolve) => { /** some async operation */ })
}
async function main() {
try {
const result = await mightThrowAnError()
console.log(result)
} catch(err) {
// Recover somehow!
}
}
main()
}
You don't have to immediately await
the return value from a function. You can store the reference to the Promise and await
it later. Or build an array of Promises, and await
them all at once use await Promise.all(operations)
.
async function main() {
const pendingOperation = maybeAsyncOperation()
// ------------------------------------
// do a bunch of things while that operation is in-progress
// ------------------------------------
const result = await pendingOperation;
console.log(result)
}
To the runtime, async
can be considered a flag that there is an implicit Promise returned from your function that is resolved once your function is finished. It's also a flag to any higher-level function that to access your function's return value, or to ensure that your function is finished executing before continuing, you must use the await
operator on it.
For example:
// Sends three distinct HTTP requets one by one, gets the JSON response value, and returns the values as an array.
async function doThreeRequests() {
const results = []
const requestUrls = ['/url1', '/url2', '/url3']
for (const requestUrl of requestUrls) {
const response = await fetch(requestUrl)
results.push(await response.json())
}
return results
}
async function main() {
// Despite the code for doThreeRequests looking synchronous, it's actually returning a Promise
// that must be awaited in order to access doThreeRequests's return value.
const [one, two, three] = await doThreeRequests()
}
main()