How to use async/await in JavaScript
By Flavio Copes
Learn how to use async/await in JavaScript, the ES2017 syntax built on promises that makes asynchronous code look synchronous and easier to read and debug.
- Introduction
- Why were async/await introduced?
- How it works
- A quick example
- Promise all the things
- Handle errors with try and catch
- The code is much simpler to read
- Multiple async functions in series
- Run independent work concurrently
- Easier debugging
Introduction
async and await let you write promise-based code with a syntax that reads from top to bottom.
An async function always returns a promise. await pauses that function until the promise settles, while the rest of the JavaScript program can keep running.
The syntax was standardized in ES2017. It consumes promises; it does not replace them. See the MDN async function reference for the precise behavior.
Why were async/await introduced?
Promises solve many problems created by deeply nested callbacks. Long promise chains can still be difficult to read.
async and await make asynchronous control flow look closer to synchronous code. They also let you use normal try...catch blocks for errors.
How it works
An async function returns a promise, like in this example:
const doSomethingAsync = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 3000)
})
}
Use await before a promise to get its fulfilled value:
const doSomething = async () => {
const result = await doSomethingAsync()
console.log(result)
}
Only the surrounding async function pauses. The main thread is not blocked.
In regular JavaScript code, await must be inside an async function. You can also use it at the top level of a JavaScript module.
A quick example
This is a simple example of async/await used to run a function asynchronously:
const doSomethingAsync = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 3000)
})
}
const doSomething = async () => {
console.log(await doSomethingAsync())
}
console.log('Before')
doSomething()
console.log('After')
The above code will print the following to the browser console:
Before
After
I did something //after 3s
Promise all the things
Prepending the async keyword to any function means that the function will return a promise.
Even if it’s not doing so explicitly, it will internally make it return a promise.
This is why this code is valid:
const aFunction = async () => {
return 'test'
}
aFunction().then(alert) // This will alert 'test'
It behaves like this:
const aFunction = async () => {
return Promise.resolve('test')
}
aFunction().then(alert) // This will alert 'test'
The returned promise is always a new promise, even if the async function returns an existing promise.
Handle errors with try and catch
If an awaited promise rejects, await throws the rejection reason.
Use try...catch to handle it:
const loadUser = async () => {
try {
const response = await fetch('/user.json')
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
return await response.json()
} catch (error) {
console.error(error)
}
}
If you don’t catch the error, the promise returned by the async function rejects.
The code is much simpler to read
As you can see in the example above, our code looks very simple. Compare it to code using plain promises, with chaining and callback functions.
And this is a very simple example, the major benefits will arise when the code is much more complex.
For example here’s how you would get a JSON resource, and parse it, using promises:
const getFirstUserData = () => {
return fetch('/users.json') // get users list
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
return response.json()
})
.then(users => users[0]) // pick first user
.then(user => fetch(`/users/${user.name}`)) // get user data
.then(userResponse => {
if (!userResponse.ok) {
throw new Error(`HTTP error: ${userResponse.status}`)
}
return userResponse.json()
})
}
getFirstUserData()
And here is the same functionality provided using await/async:
const getFirstUserData = async () => {
const response = await fetch('/users.json') // get users list
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
const users = await response.json() // parse JSON
const user = users[0] // pick first user
const userResponse = await fetch(`/users/${user.name}`) // get user data
if (!userResponse.ok) {
throw new Error(`HTTP error: ${userResponse.status}`)
}
const userData = await userResponse.json() // parse JSON
return userData
}
getFirstUserData()
Multiple async functions in series
Async functions can be chained very easily, and the syntax is much more readable than with plain promises:
const promiseToDoSomething = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 10000)
})
}
const watchOverSomeoneDoingSomething = async () => {
const something = await promiseToDoSomething()
return something + ' and I watched'
}
const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
const something = await watchOverSomeoneDoingSomething()
return something + ' and I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then(res => {
console.log(res)
})
It prints:
I did something and I watched and I watched as well
Run independent work concurrently
Consecutive await expressions run in sequence:
const user = await loadUser()
const posts = await loadPosts()
If the operations don’t depend on each other, start them together and await Promise.all():
const [user, posts] = await Promise.all([
loadUser(),
loadPosts()
])
This also connects both operations to the same promise chain, so one try...catch can handle a rejection.
Easier debugging
Async functions often make stack traces and breakpoints easier to follow than long promise chains.
They are still asynchronous. The debugger may pause and resume the function around each await.
Related posts about js: