AlgoInsight

Free lesson · JavaScript Basic to Advance

async/await: Promises in Disguise

Easy5 min read

Async Code That Reads Like Sync Code

async/await is syntax for working with Promises that makes async code look and feel synchronous. It was added in ES2017 and has become the dominant way to write async JavaScript in modern code.

function fetchUser(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id, name: `User_${id}` }), 50);
  });
}

// Promise chain version
function loadUserPromises(id) {
  return fetchUser(id)
    .then(user => {
      console.log("Promise chain got:", user);
      return user;
    });
}

// async/await version — identical behavior
async function loadUserAsync(id) {
  const user = await fetchUser(id);
  console.log("async/await got:", user);
  return user;
}

loadUserPromises(1);
loadUserAsync(2);

Both functions do the same thing. The async/await version is shorter and reads top-to-bottom like normal code. There's no .then. No callback. Just a function call that returns the data.

But — and this is critical — async/await is not new behavior. It's syntactic sugar over Promises. Every async function returns a Promise. Every await expression awaits a Promise. The whole mechanism Topic 9.4 covered is still what's running underneath. The syntax just hides it.

This Topic covers what async and await actually do, how they map to the Promise concepts you already know, and the patterns they make easier.

Interactive animation: The same four steps written with await and as the .then chain beneath - one machine, two spellings.

What async Does to a Function

Marking a function as async does two things:

1. The function automatically returns a Promise. Whatever you return from inside is wrapped in Promise.resolve(...). If you throw, the returned Promise rejects with the thrown error.

async function returnsValue() {
  return 42;
}

async function returnsPromise() {
  return Promise.resolve(99);   // a Promise inside another Promise gets flattened
}

async function throwsError() {
  throw new Error("Oops");
}

returnsValue().then(v => console.log("returnsValue:", v));
returnsPromise().then(v => console.log("returnsPromise:", v));
throwsError().catch(e => console.log("throwsError:", e.message));

All three functions return Promises. The first wraps 42 in Promise.resolve(42). The second flattens the inner Promise into a single Promise — async functions always return a single, non-nested Promise. The third's throw becomes a rejection.

2. The function gains the ability to use await. You can only use await inside an async function (or at the top level of modern modules — covered in Module 10).

Interactive animation: An async function that returns a plain string still hands its caller a promise, unwrapped later with .then.

What await Does

await pauses the execution of an async function until the awaited Promise settles. When the Promise resolves, the value of the await expression is the resolved value. When the Promise rejects, the await expression throws the rejection.

async function example() {
  console.log("Before await");

  const value = await new Promise(resolve => {
    setTimeout(() => resolve("got it"), 50);
  });

  console.log("After await:", value);
  return value;
}

console.log("Calling function");
example().then(v => console.log("Final:", v));
console.log("After calling");

The output order: Calling function, Before await, After calling, then 50ms later After await: got it, then Final: got it.

Trace through it:

  1. example() is called. Synchronous code runs inside it: console.log("Before await") and starts the await expression.
  2. await sees a Promise. The async function effectively pauses here. Control returns to the caller.
  3. The caller's synchronous code continues: console.log("After calling").
  4. Main code finishes. The microtask queue has nothing yet (the Promise is still pending).
  5. 50ms later, the timer fires. The Promise resolves.
  6. The async function resumes after the await, with value set to "got it".
  7. console.log("After await: got it") runs.
  8. The function returns. The Promise returned from example() resolves with that value.
  9. The .then runs: console.log("Final: got it").

The "pausing" is implemented via Promise mechanics. The body of the async function after the await becomes a callback attached to the awaited Promise. From a runtime perspective, await is essentially a .then in disguise.

graph TD
    A[async function called] --> B[Synchronous code runs]
    B --> C[await expression]
    C --> D[Function pauses<br/>control returns to caller]
    D --> E[Awaited Promise resolves]
    E --> F[Function resumes after await]
    F --> G[More sync code]
    G --> H[Returns / next await]

    style C fill:#7c3aed,color:#fff
    style E fill:#10b981,color:#fff
    style F fill:#7c3aed,color:#fff
This diagram shows how an async function executes. The function starts running synchronously. When it hits an await expression, the function pauses, and control returns to whoever called it. Meanwhile, the awaited Promise is doing its work. When the Promise resolves, the async function resumes execution after the await with the resolved value, continues running synchronously until the next await or until it returns.

Sequential vs Parallel With await

Be careful: await is sequential by default. Two awaits in a row run their operations one after the other, not in parallel.

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

async function sequential() {
  console.time("sequential");
  const a = await delay(100, "A");
  const b = await delay(100, "B");
  const c = await delay(100, "C");
  console.timeEnd("sequential");
  return [a, b, c];
}

async function parallel() {
  console.time("parallel");
  const [a, b, c] = await Promise.all([
    delay(100, "A"),
    delay(100, "B"),
    delay(100, "C")
  ]);
  console.timeEnd("parallel");
  return [a, b, c];
}

sequential().then(r => console.log("Sequential:", r));
parallel().then(r => console.log("Parallel:", r));

sequential takes about 300ms — three 100ms operations one after another. parallel takes about 100ms — three 100ms operations running simultaneously.

The trap: writing sequential awaits when the operations don't depend on each other is a real performance bug. If getUser, getOrders, and getProducts could all run in parallel, awaiting them sequentially makes your code three times slower than necessary.

The pattern to remember:

We cover Promise.all and its siblings in detail in Topic 9.8.

Interactive animation: Two independent requests awaited in series take 600ms - start both first, then await them, and it takes about 300ms.

How async Functions Look Like Sync Code

The reason async/await won so quickly: it makes async code structurally identical to sync code. Compare:

// Sync version
function processSyncStyle(input) {
  const step1 = input * 2;
  const step2 = step1 + 10;
  const step3 = step2.toString();
  return step3;
}

// Async version with async/await
async function processAsyncStyle(input) {
  const step1 = await Promise.resolve(input * 2);
  const step2 = await Promise.resolve(step1 + 10);
  const step3 = await Promise.resolve(step2.toString());
  return step3;
}

console.log(processSyncStyle(5));
processAsyncStyle(5).then(r => console.log(r));

The two functions have nearly identical structure. The async version just has await before each step. There's no .then chain, no nesting, no callback management. The code reads as a linear sequence of steps.

This is the win. async code with async/await reads almost identically to sync code, with the await keyword as the only visible marker. Once you internalize the rule "every await pauses, every async returns a Promise," you stop having to think about async mechanics most of the time.

Things That Stay Important to Know

async/await is great, but it hides Promise mechanics that still matter:

Every async function returns a Promise. Even if you forget. So async function f() { return 5; } and f() is a Promise, not the number 5. You have to await it or .then it.

The microtask queue is still doing the work. When an await resumes, it's via a microtask. All the event-loop rules from Topic 9.2 still apply. Mixing await with setTimeout gives the same priority ordering as mixing .then with setTimeout.

Errors propagate via Promise rejection. A throw inside an async function becomes a Promise rejection. We cover this in Topic 9.7.

You can't await outside an async function. At the module top level you can (covered in Module 10). Inside a regular function, await is a syntax error.

Interactive animation: What await does with a fulfilled promise, a plain value, a rejected promise, and at the top of a module.

Common Pitfalls

Warning: The async/await traps.

Forgetting that async functions return Promises. Calling an async function gives you a Promise, not the value. You either await it (inside another async function) or .then it.

Sequential await when parallel would work. If two operations are independent, do Promise.all([op1(), op2()]) instead of await op1(); await op2(). The latter is significantly slower for no reason.

Using await with values that aren't Promises. await 5 works — it wraps the value in Promise.resolve(5) and immediately resumes. But it adds a microtask delay for no reason. Don't await things that aren't actually Promises.

Mixing .then with await unnecessarily. Both work, but consistency matters. Pick one style per function. await is usually clearer for sequential code.

Why This Matters in Interviews

Interview: "What does async/await do? How is it different from Promises?" is a top JavaScript interview question. The expected answer: async/await is syntactic sugar over Promises. async functions return Promises. await pauses execution and resumes when the awaited Promise settles. The mechanism is identical to chained .thens, but the syntax reads sequentially.

"What's the difference between sequential and parallel awaits?" is a senior follow-up. Expected answer: each await waits for the previous one. For independent operations, use Promise.all to run them in parallel.

Topic 9.12 — Interview Lens: Promise and async/await Patterns — covers the deeper treatment.

Try It

Exercise: Convert this Promise chain to async/await:

function loadDashboard(userId) {
  return fetchUser(userId)
    .then(user => {
      return fetchUserOrders(user.id).then(orders => ({ user, orders }));
    })
    .then(({ user, orders }) => {
      return fetchRecommendations(user.id).then(recs => ({ user, orders, recs }));
    });
}

After converting to async/await, identify: are these operations actually sequential, or could some run in parallel? Refactor for parallel where possible.

Hint: the first call requires the user, but the orders and recommendations only need user.id. They can probably run in parallel after the first call completes.

Back to JavaScript Basic to Advance