Skip to content
JavaScript js async 4 min read

async / await

async/await is syntactic sugar built on top of promises that lets you write asynchronous code that reads top-to-bottom like ordinary synchronous code. Instead of chaining .then() callbacks, you await a promise and the resulting value is assigned to a variable. The control flow becomes linear, errors flow through normal try/catch, and the underlying promise machinery stays exactly the same. This is the modern default for working with asynchronous operations in both the browser and Node.js.

Async functions always return a promise

Marking a function with the async keyword changes one thing about its return value: it is always wrapped in a promise. If you return a plain value, the function returns a promise that fulfills with that value. If you throw, it returns a promise that rejects with that error.

async function getUser() {
  return { id: 1, name: "Ada" };
}

const result = getUser();
console.log(result);                 // a Promise, not the object
getUser().then((user) => console.log(user.name));

Output:

Promise { { id: 1, name: 'Ada' } }
Ada

Because the return value is a promise, an async function is fully interoperable with any promise-based API — you can .then() it, await it, or pass it to Promise.all().

await pauses inside the function

The await operator can only be used inside an async function (or at the top level of an ES module). It pauses execution of that function until the awaited promise settles, then resumes with the fulfilled value. Crucially, it does not block the main thread — the engine is free to run other code while the function is suspended.

async function loadProfile() {
  console.log("fetching...");
  const res = await fetch("https://api.example.com/user/1");
  const user = await res.json();
  console.log(`Loaded ${user.name}`);
  return user;
}

loadProfile();
console.log("this logs before the fetch resolves");

Output:

fetching...
this logs before the fetch resolves
Loaded Ada

If the awaited promise rejects, await throws the rejection reason as an exception at that line, which you can catch with try/catch.

async function safeLoad() {
  try {
    const res = await fetch("https://api.example.com/missing");
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error("Failed:", err.message);
    return null;
  }
}

Note: await only works inside async functions. At the top level of a <script type="module"> or an ES module file you can use top-level await, but in a CommonJS file or a regular function you cannot.

Sequential vs parallel awaits

A common performance trap is awaiting independent operations one after another. Each await waits for the previous one to finish before starting the next, even when they don’t depend on each other.

// Sequential: total time = sum of all durations (~3s)
async function sequential() {
  const a = await delay(1000, "A");
  const b = await delay(1000, "B");
  const c = await delay(1000, "C");
  return [a, b, c];
}

function delay(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

When operations are independent, start them all first and await them together with Promise.all(). The requests now overlap, so the total time is the duration of the slowest one rather than the sum.

function delay(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

// Parallel: total time = the longest single duration (~1s)
async function parallel() {
  const [a, b, c] = await Promise.all([
    delay(1000, "A"),
    delay(1000, "B"),
    delay(1000, "C"),
  ]);
  return [a, b, c];
}

parallel().then((values) => console.log(values));

Output:

[ 'A', 'B', 'C' ]
PatternWhen to useTotal time (3 × 1s tasks)
await one at a timeEach step depends on the previous result~3s
await Promise.all([...])Steps are independent, need all results~1s
await Promise.allSettled([...])Independent, some may fail, want all outcomes~1s

Converting chains to async/await

A promise chain translates almost mechanically into async/await. Each .then(value => ...) becomes an await assignment, and a trailing .catch() becomes a try/catch block.

// Promise chain
function loadOrders(userId) {
  return fetch(`/api/users/${userId}`)
    .then((res) => res.json())
    .then((user) => fetch(`/api/orders?customer=${user.id}`))
    .then((res) => res.json())
    .then((orders) => orders.filter((o) => o.status === "open"))
    .catch((err) => {
      console.error(err);
      return [];
    });
}

The async/await version expresses the same logic with flat, readable steps and named intermediate variables.

async function loadOrders(userId) {
  try {
    const userRes = await fetch(`/api/users/${userId}`);
    const user = await userRes.json();

    const orderRes = await fetch(`/api/orders?customer=${user.id}`);
    const orders = await orderRes.json();

    return orders.filter((o) => o.status === "open");
  } catch (err) {
    console.error(err);
    return [];
  }
}

Both functions return a promise and behave identically to callers — the rewrite is purely about readability and easier debugging (real stack traces, normal breakpoints, standard try/catch).

Best Practices

  • Use Promise.all() for independent operations; reserve sequential await for steps that genuinely depend on prior results.
  • Wrap awaits that can reject in try/catch, or handle the rejection at the call site — unhandled rejections crash Node processes.
  • Don’t put await inside a for...of loop over independent items; map to promises and await Promise.all() instead.
  • Remember await only unwraps the value — check res.ok yourself, since fetch() does not reject on HTTP error statuses.
  • Prefer async/await over long .then() chains for new code, but keep them interchangeable since both produce and consume promises.
  • Use Promise.allSettled() when you want every result even if some operations fail.
Last updated June 1, 2026
Was this helpful?