JavaScriptasync / await

async / await

async and await are syntactic sugar over promises. An async function always returns a promise, and await lets you pause inside it until a promise settles — making asynchronous code read like the synchronous version. You still get the same event-loop behaviour and the same promises; you just stop wiring up .then chains by hand.

The basics

Put async in front of any function and it becomes asynchronous. Inside, await someExpression waits for the expression's promise (or wraps a plain value in one) and returns its fulfilment value. If the promise rejects, the rejection is thrown like a regular exception.

JS
async function greet() {
  return "hello";          // automatically wrapped in Promise.resolve
}

greet().then((v) => console.log(v));   // "hello"

async function loadUser(id) {
  const response = await fetch(`/api/user/${id}`);
  const user = await response.json();
  return user;
}

The same function written with .then is longer and noisier:

Plain promise version

JS
function loadUser(id) {
  return fetch(`/api/user/${id}`).then((response) => response.json());
}
The mental model

Two ideas explain almost every async bug:

  • Calling an async function returns a promise immediately, even before any await inside it runs. The function only pauses internally at each await.

  • await x is roughly Promise.resolve(x).then(...): it schedules the rest of the function as a microtask and yields control. Other code keeps running in the meantime.

JS
async function f() {
  console.log("a");
  await 0;             // splits the function into two microtasks
  console.log("c");
}

console.log("1");
f();
console.log("2");
1
a
2
c

After await 0, the rest of f is parked as a microtask. The synchronous "2" runs before "c".

try / catch around await

Because rejected promises become thrown errors, you handle async failures with the same try / catch you already know. No special .catch syntax is needed.

JS
async function loadProfile(id) {
  try {
    const response = await fetch(`/api/user/${id}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } catch (err) {
    console.error("failed to load", id, err);
    return null;
  }
}
Return await vs return
Inside a `try` you usually want `return await promise` — without `await`, a rejection escapes the surrounding `try` because the function has already returned. Outside a `try`, `return promise` is fine and slightly cheaper.
Running things in parallel

A common mistake is to await independent tasks in sequence and pay for their times added together:

Slow — sequential

JS
async function loadDashboard() {
  const user = await fetch("/api/user").then((r) => r.json());
  const posts = await fetch("/api/posts").then((r) => r.json());
  const tags = await fetch("/api/tags").then((r) => r.json());
  return { user, posts, tags };
}

Kick the work off first, then await the resulting promises together with Promise.all:

Fast — parallel

JS
async function loadDashboard() {
  const [user, posts, tags] = await Promise.all([
    fetch("/api/user").then((r) => r.json()),
    fetch("/api/posts").then((r) => r.json()),
    fetch("/api/tags").then((r) => r.json()),
  ]);
  return { user, posts, tags };
}

Sequence only what truly depends on the previous step. Everything else belongs in a combinator.

Loops and await

for and for...of cooperate with await — each iteration waits before starting the next. forEach does not: the callback is async, but forEach does not await it, so the loop finishes "too early".

JS
// Sequential — one at a time.
for (const id of ids) {
  await processOne(id);
}

// Parallel — all at once.
await Promise.all(ids.map(processOne));

// BUG: looks sequential, isn't.
ids.forEach(async (id) => {
  await processOne(id);   // forEach throws away the returned promise
});
Common bugs
  • Forgotten await. const data = fetch(url) assigns a pending promise to data. Reading data.title later is undefined.

  • Awaiting too late. Storing a promise in a variable is fine — just await it before you use the value.

  • Mixing .then and await carelessly. Pick one style per function; the chains can be hard to read otherwise.

  • Top-level await outside modules. It only works in ES modules. In a classic <script> you need an async wrapper.

  • Swallowed rejections. A for...of loop without try/catch propagates the rejection, but a forEach callback hides it because the returned promise is dropped.

Forgotten await

JS
async function bad() {
  const user = fetch("/me").then((r) => r.json());
  console.log(user.name);     // "Cannot read properties of undefined (reading 'name')"
}

async function good() {
  const user = await fetch("/me").then((r) => r.json());
  console.log(user.name);
}
async functions are still promises

An async function is a normal promise-returning function — you can call .then/.catch on it, pass it to Promise.all, or await it from another async function. Throwing inside the function becomes a rejected promise outside it.

JS
async function fail() {
  throw new Error("nope");
}

fail().catch((err) => console.log(err.message));   // "nope"
Style heuristic
If a function does any async work, mark it `async` and use `await` throughout. Mixing a single `.then` into an otherwise-await function almost always reads worse than rewriting it.