Q17 of 40 · JavaScript

When should you use async/await over `.then()` chains?

JavaScriptMidjavascriptasync-awaitpromisesasyncreadability

Short answer

Short answer: `async`/`await` makes sequential async steps read like synchronous code, improving readability and debugging (better stack traces). `.then()` chains can be cleaner for parallel fan-out. Both compile to the same underlying Promise machinery — choice is about readability.

Detail

async/await is syntactic sugar over Promises — every async function returns a Promise, and await unwraps a Promise's value, pausing execution of that function only (not the whole thread).

When async/await is better:

  • Sequential steps with error handling: try/catch is cleaner than deeply nested .catch() handlers
  • Debugging: stack traces show the line with await, making the origin of an error clearer
  • Conditional async logic: if (condition) { await doA(); } else { await doB(); } is unreadable with chains

When .then() is better:

  • Parallel fan-out: Promise.all([fetch(a), fetch(b)]) reads more naturally than naming two variables
  • Composing pipelines as data that are passed to multiple consumers

Common mistake — sequential await in a loop: Using await inside .map() makes each iteration sequential. Use await Promise.all(arr.map(async item => ...)) for parallel.

// EXAMPLE

// .then() — clean for parallel
const [user, posts] = await Promise.all([
  fetch("/api/user").then(r => r.json()),
  fetch("/api/posts").then(r => r.json()),
]);

// async/await — clean for sequential steps with error handling
async function loginAndFetch(creds) {
  try {
    const loginRes = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify(creds),
    });
    if (!loginRes.ok) throw new Error("Login failed");
    const { token } = await loginRes.json();

    const profile = await fetch("/api/profile", {
      headers: { Authorization: `Bearer ${token}` },
    });
    return profile.json();
  } catch (err) {
    console.error("Auth error:", err.message);
    throw err;
  }
}

// MODEL ANSWER

Both are the same underlying mechanism — async and await are syntactic sugar over Promises. Every async function returns a Promise and await unwraps it, pausing execution of that function only, not the whole thread. The choice between them is purely about readability. I reach for async and await when I have sequential steps that depend on each other, especially where I need error handling — try and catch around sequential awaits is much cleaner than nested catch handlers, and when something throws, the stack trace points at the awaited line. I reach for then chains when I am running things in parallel — Promise.all with two fetch calls in a list reads naturally as a single parallel operation. The pitfall worth calling out explicitly: using await inside map makes each iteration run sequentially, because map returns an array of Promises rather than waiting for them. The correct pattern for parallel array processing is await Promise.all wrapping arr.map with an async function. That is probably the single most common async question at mid level, and getting it right signals real experience with the concurrency model.

// WHAT INTERVIEWERS LOOK FOR

Understanding that both are the same underlying mechanism and choice is about readability. The parallel-await-in-map pitfall is a common interview focus — knowing `await Promise.all(arr.map(...))` demonstrates real async experience.

// COMMON PITFALL

Awaiting inside a `.map()` — each iteration awaits in sequence (no parallelism) and the result is an array of Promises, not resolved values. Use `await Promise.all(arr.map(async item => ...))` for true parallel execution.