Skip to content
FLAVIO COPES
flaviocopes.com
2026

How to use async/await in JavaScript

By

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

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: