Home/Learn/JavaScript & TypeScript/async / await — Promises That Read Like Sequential Code

async / await — Promises That Read Like Sequential Code

Intermediate
Async

async functions always return a promise; await pauses inside them until a promise settles. It is syntax over promises, so everything about promises still applies.

Overview

await made asynchronous code readable by letting it look sequential, which is why almost all modern code uses it over .then chains. The mental model to hold: an async function returns a promise immediately, and await suspends only that function while the rest of the program keeps running. Errors become throwable, so try/catch works — a genuine improvement over .catch, which was easy to attach in the wrong place. The main hazards are forgetting await entirely, and awaiting inside a loop when the iterations are independent.

The Translation

Every async/await form has a promise equivalent. Knowing both makes reading other people's code easier.

Same behaviour, two syntaxes
// async/await
async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`)
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return await res.json()
  } catch (err) {
    logError(err)
    throw err                     // re-throw or the caller sees success
  } finally {
    setLoading(false)
  }
}

// the same thing with promises
function loadUser(id) {
  return fetch(`/api/users/${id}`)
    .then(res => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      return res.json()
    })
    .catch(err => { logError(err); throw err })
    .finally(() => setLoading(false))
}

The Two Common Mistakes

Missing await, and awaiting in a loop. Both are silent — the code runs, it is just wrong.

Missing await, and the loop trap
// 1. Forgotten await — you get a Promise, not the value
const user = fetchUser()          // Promise { <pending> }
console.log(user.name)            // undefined
// A forgotten await also escapes try/catch: the rejection
// becomes an unhandled rejection instead of being caught.

// 2. Awaiting in a loop — sequential when it need not be
for (const id of ids) {
  results.push(await fetchOne(id))    // N x latency
}

// Parallel:
const results = await Promise.all(ids.map(fetchOne))

// Sequential is right when order matters or you are rate-limited:
for (const id of ids) {
  await processInOrder(id)
}

Top-Level await and Async Iteration

Modules can await at the top level; for await consumes async iterables such as streams.

Top-level await and async generators
// Top-level await — ES modules only, not CommonJS
const config = await loadConfig()
export default config

// for await — streams, paginated APIs, anything async-iterable
async function* pages(url) {
  let next = url
  while (next) {
    const res = await fetch(next)
    const data = await res.json()
    yield data.items
    next = data.next
  }
}

for await (const items of pages('/api/problems')) {
  render(items)
}

Key Points to Remember

  • 1An async function always returns a promise, even when it returns a plain value
  • 2await suspends only the containing function — the rest of the program keeps running
  • 3try/catch works with await, but a forgotten await escapes it and becomes an unhandled rejection
  • 4Awaiting inside a loop is sequential; use Promise.all with map when the iterations are independent
  • 5Top-level await works in ES modules only, and for await consumes async iterables like paginated APIs

Interview Questions

Sign in to ask Aria
1

What does an async function return if you return a plain string from it?

Easy
2

What goes wrong if you forget the await keyword?

Medium
3

When is awaiting inside a for loop the correct choice rather than a bug?

Medium

Ask Aria about async / await — Promises That Read Like Sequential Code

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…