Free lesson · JavaScript Basic to Advance
async/await: Promises in Disguise
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:
example()is called. Synchronous code runs inside it:console.log("Before await")and starts theawaitexpression.awaitsees a Promise. Theasyncfunction effectively pauses here. Control returns to the caller.- The caller's synchronous code continues:
console.log("After calling"). - Main code finishes. The microtask queue has nothing yet (the Promise is still pending).
- 50ms later, the timer fires. The Promise resolves.
- The
asyncfunction resumes after theawait, withvalueset to "got it". console.log("After await: got it")runs.- The function returns. The Promise returned from
example()resolves with that value. - The
.thenruns: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:
- Sequential when there's a dependency:
const user = await getUser(); const orders = await getOrders(user.id); - Parallel when there isn't:
const [user, settings] = await Promise.all([getUser(id), getSettings(id)]);
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
asyncfunctions return Promises. Calling anasyncfunction gives you a Promise, not the value. You eitherawaitit (inside another async function) or.thenit.Sequential
awaitwhen parallel would work. If two operations are independent, doPromise.all([op1(), op2()])instead ofawait op1(); await op2(). The latter is significantly slower for no reason.Using
awaitwith values that aren't Promises.await 5works — it wraps the value inPromise.resolve(5)and immediately resumes. But it adds a microtask delay for no reason. Don'tawaitthings that aren't actually Promises.Mixing
.thenwithawaitunnecessarily. Both work, but consistency matters. Pick one style per function.awaitis usually clearer for sequential code.
Why This Matters in Interviews
Interview: "What does
async/awaitdo? How is it different from Promises?" is a top JavaScript interview question. The expected answer:async/awaitis syntactic sugar over Promises.asyncfunctions return Promises.awaitpauses 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
awaitwaits for the previous one. For independent operations, usePromise.allto 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.