JavaScript Interview Prep
Asynchronous JavaScript

async/await

Syntactic Sugar That Reads Like Synchronous Code

LinkedIn Hook

async/await is the feature that made asynchronous JavaScript finally readable. But the exact reason it feels so natural is also the reason it hides some of the nastiest performance bugs in modern codebases.

await in a for loop is the most famous example — it looks clean, it reads like synchronous code, and it quietly turns three 500ms parallel requests into a 1500ms sequential chain. Multiply that across a real product and you've just burned seconds of user-perceived latency for no reason.

In this lesson you'll see the full evolution — callbacks to Promises to async/await — side by side, learn exactly what the async keyword guarantees about your return value, understand why forgetting await produces a bug that's truthy and silent, and spot the classic "await in a loop" anti-pattern before it ships to production.

If you write await every day but can't explain what an async function actually returns — this lesson fixes that.

Read the full lesson -> [link]

#JavaScript #InterviewPrep #AsyncAwait #AsyncJS #Frontend #CodingInterview #WebDevelopment


async/await thumbnail


What You'll Learn

  • What async/await actually is (spoiler: syntactic sugar over Promises) and what an async function returns
  • How to rewrite Promise chains with try/catch for linear, readable error handling
  • The "await in a loop" anti-pattern and when sequential vs parallel is the right choice

What Is async/await?

If Promises are the blueprint, async/await is the beautiful house built on top of it. It's syntactic sugar over Promises — same engine underneath, but code that reads like synchronous logic.

Imagine reading a recipe: "First get the flour, then add water, then knead the dough." That's how async/await reads. Without it (using .then() chains), the recipe would read more like: "Get the flour, and when you have it, add water, and when that's done, knead the dough."


The Full Evolution: Callback -> Promise -> async/await

// ============ CALLBACK STYLE ============
function getUserCallback(id, callback) {
  setTimeout(() => callback(null, { id, name: "Rakibul" }), 500);
}
function getOrdersCallback(userId, callback) {
  setTimeout(() => callback(null, [{ id: 101, item: "Laptop" }]), 500);
}

getUserCallback(1, (err, user) => {
  if (err) return console.error(err);
  getOrdersCallback(user.id, (err, orders) => {
    if (err) return console.error(err);
    console.log(orders); // nested, messy
  });
});

// ============ PROMISE STYLE ============
function getUserPromise(id) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: "Rakibul" }), 500);
  });
}
function getOrdersPromise(userId) {
  return new Promise((resolve) => {
    setTimeout(() => resolve([{ id: 101, item: "Laptop" }]), 500);
  });
}

getUserPromise(1)
  .then((user) => getOrdersPromise(user.id))
  .then((orders) => console.log(orders)) // flat, better
  .catch((err) => console.error(err));

// ============ ASYNC/AWAIT STYLE ============
async function fetchUserOrders() {
  try {
    const user = await getUserPromise(1);
    const orders = await getOrdersPromise(user.id);
    console.log(orders); // reads like sync code!
  } catch (err) {
    console.error(err);
  }
}
fetchUserOrders();

Error Handling with try/catch

async function loadDashboard() {
  try {
    const user = await fetchUser();
    const profile = await fetchProfile(user.id);
    const notifications = await fetchNotifications(user.id);

    renderDashboard({ user, profile, notifications });
  } catch (error) {
    // Catches any rejection from any of the awaits above
    console.error("Dashboard load failed:", error.message);
    showErrorUI();
  } finally {
    hideLoadingSpinner();
  }
}

One try/catch is enough for the whole block — any rejected await jumps control straight into the catch, just like a synchronous throw.


The "await in a Loop" Antipattern

This is one of the most common performance mistakes — and a frequent interview question:

// BAD: Sequential — each request waits for the previous one
// Total time: 500 + 500 + 500 = 1500ms
async function getSequential(userIds) {
  const users = [];
  for (const id of userIds) {
    const user = await fetchUser(id); // waits each time!
    users.push(user);
  }
  return users;
}

// GOOD: Parallel — all requests fire at once
// Total time: ~500ms (slowest single request)
async function getParallel(userIds) {
  const promises = userIds.map((id) => fetchUser(id)); // fire all at once
  const users = await Promise.all(promises); // wait for all
  return users;
}

When to use sequential: When each request depends on the result of the previous one (e.g., paginated APIs where page 2's URL comes from page 1's response).

When to use parallel: When requests are independent (e.g., fetching data for multiple users).


What async Functions Actually Return

An async function always returns a Promise:

async function add(a, b) {
  return a + b; // automatically wrapped in Promise.resolve()
}

add(2, 3).then((result) => console.log(result)); // 5

// Even throwing an error returns a rejected Promise
async function fail() {
  throw new Error("boom");
}

fail().catch((err) => console.error(err.message)); // "boom"

async/await visual 1


Common Mistakes

  • Forgetting await before a Promise — you get the Promise object (which is truthy) instead of its resolved value, producing silent bugs like if (fetchUser()) always being true.
  • Using await inside a for loop for independent requests — you turn what should be parallel work into a sequential chain and inflate latency linearly.
  • Assuming a try/catch inside a non-async function can catch an awaited rejection — await only exists inside async functions (or top-level in ES modules); using it elsewhere is a syntax error.

Interview Questions

Q: What does the async keyword do?

It marks a function as asynchronous and guarantees it returns a Promise. If the function returns a non-Promise value, it's automatically wrapped in Promise.resolve(). If the function throws, it returns a rejected Promise.

Q: What happens if you forget to use await before a Promise?

You get the Promise object itself, not its resolved value. The code continues without waiting for the async operation. This is a common bug — if (fetchUser()) is always truthy because a pending Promise is a truthy object.

Q: Why is await inside a for loop often a performance problem?

Each iteration waits for the previous await to complete before starting the next one, making requests sequential. If the operations are independent, use Promise.all() to run them in parallel.

Q: Can you use await at the top level?

Yes, in ES modules (type="module" in browsers, or .mjs files in Node.js). It's called "top-level await." In CommonJS (require-based) modules, you need to wrap it in an async IIFE.

Q: What does the async keyword guarantee about a function's return value?

The function always returns a Promise, regardless of what's inside — a returned value is wrapped in Promise.resolve(value), a thrown error produces Promise.reject(error), and a returned Promise is passed through (the outer async function's Promise settles when the inner one does).

Q: What happens if you await a non-Promise value?

It's wrapped in Promise.resolve(value) on the spot, the current function pauses for one microtask, then resumes with that value. So await 42 is valid and yields 42 — just with a microtask delay.


Quick Reference — Cheat Sheet

ASYNC / AWAIT — QUICK MAP

Shape:
  async function fn() {
    try {
      const result = await somePromise();
    } catch (err) {
      // handle rejection
    } finally {
      // cleanup
    }
  }

Guarantees:
  - async fn always returns a Promise
  - return value -> Promise.resolve(value)
  - thrown error -> Promise.reject(error)
  - await unwraps a Promise; await 42 works too

Sequential vs Parallel:
  // Sequential (slow, 3 * 500ms)
  for (const id of ids) await fetchUser(id);

  // Parallel (fast, ~500ms)
  await Promise.all(ids.map(id => fetchUser(id)));

Top-level await:
  Allowed in ES modules; wrap in async IIFE in CommonJS.

Previous: Promises — Chaining & Error Handling Next: Promise Combinators — all, race, allSettled, any


This is Lesson 3.8 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page