Lesson 25·M3 Async JavaScript
async / await
OpenLearning Objectives
By the end of this lesson you should be able to:
- Explain what
asyncdoes to a function's return value - Explain what
awaitactually does to the function's execution - Rewrite an
async/awaitsnippet as equivalent.thenchains - Say why
awaitin aforEachdoesn't wait, and fix it - Answer the "is
awaitblocking?" question precisely
What is async / await?
async makes a function return a promise; await pauses the function until a promise settles, then resumes on the microtask queue.
An async function never returns its literal value:
async function greet(name) {
return `Hello, ${name}!`;
}
console.log(greet('Sam'));
Output:
Promise { 'Hello, Sam!' }
The function body is a promise machine. Whatever it returns becomes the promise's
fulfilment value — you read it with await or .then, never directly.
Mental Model
An async function is a synchronised runner. It runs like normal code until it hits an
await, then it taps out: the event loop is free to do anything else. When the awaited
promise settles, the runner is rescheduled as a microtask (Lesson 23) and continues from
the exact next line.
async function load() {
const a = await stepA(); // suspend here
const b = await stepB(); // resume → suspend again
return combine(a, b); // resume → settle the outer promise
}
Each await is a checkpoint. Between checkpoints, the whole app is free to run.
Visual Flow: await Is a .then
await promise
│
▼
┌─────────────────┐ settle? ┌──────────────────────────┐
│ pause function │────────────────►│ resume as a MICROTASK │
│ (yield control) │ no, still │ value lands in the await │
└─────────────────┘ pending ─────►│ expression (or throws) │
└──────────────────────────┘
Same queue, same ordering rules as Lesson 23: an await resumes before the next
macrotask, and before any .then attached later in the same tick.
How It Works: await Is Not return
await does three things: unwrap a promise, suspend the function, and resume on the
microtask queue. It is not a return — the code after it still runs, and whatever the
function eventually returns is what resolves its promise:
const p = Promise.resolve(5);
p.then((value) => {
console.log('first then:', value);
return value * 2;
}).then((value) => console.log('second then:', value));
Output:
first then: 5
second then: 10
The equivalent async/await — same microtask timeline:
const p = Promise.resolve(5);
async function run() {
const value = await p;
console.log('first then:', value);
return value * 2;
}
run().then((value) => console.log('second then:', value));
Output:
first then: 5
second then: 10
An await resumes the function exactly like a .then handler runs — as a microtask, after
the current task finishes. Everything you learned about microtask ordering in Lesson 23
applies to await verbatim.
Real Project Usage
function fetchUser(id) {
return new Promise((resolve) => setTimeout(() => resolve({ id, name: 'Sam' }), 10));
}
function fetchPosts(userId) {
return new Promise((resolve) => setTimeout(() => resolve(['post-1', 'post-2']), 10));
}
async function loadProfile(id) {
const user = await fetchUser(id); // sequential: posts need the user
const posts = await fetchPosts(user.id);
return { user, posts };
}
loadProfile(7).then((profile) => console.log(profile));
Output:
{ user: { id: 7, name: 'Sam' }, posts: [ 'post-1', 'post-2' ] }
When two calls are independent, don't await them one after another — run them in parallel:
function fetchStats() {
return new Promise((resolve) => setTimeout(() => resolve({ visits: 10 }), 10));
}
function fetchRecent() {
return new Promise((resolve) => setTimeout(() => resolve(['a', 'b']), 10));
}
async function loadDashboard() {
const [stats, recent] = await Promise.all([fetchStats(), fetchRecent()]); // parallel
return { stats, recent };
}
loadDashboard().then((d) => console.log(d));
Output:
{ stats: { visits: 10 }, recent: [ 'a', 'b' ] }
await Promise.all([…]) is the async version of Promise.all from Lesson 26 — one
resumption, two parallel requests.
Interview Explanation
asyncmarks a function as returning a promise.awaitpauses the function until the promise settles, then resumes it on the microtask queue — it's.thenwritten as syntax. The function's return value resolves its promise, and a thrown error rejects it.
Senior-Level Insights
The mid-level answer stops at "it's sugar." A senior adds what the sugar buys you:
- Errors become try/catch. A rejected
awaitthrows at the exact line — no more callback-style error plumbing. That's the entire topic of Lesson 27. - Sequencing is explicit.
awaitmakes the dependency between calls visible, andPromise.allmarks the ones that don't depend on each other. awaitis not blocking. The event loop keeps running; only this one function suspends. A senior can say it precisely: await yields control and re-enters via the microtask queue.
And the trap worth naming unprompted — forEach doesn't await:
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
(async () => {
[1, 2, 3].forEach(async (n) => { // ❌ fire-and-forget callbacks
await wait(10);
console.log('forEach tick', n);
});
console.log('forEach done'); // runs first
})();
Output:
forEach done
forEach tick 1
forEach tick 2
forEach tick 3
forEach calls the callback three times and moves on; the callbacks are three separate
async functions racing in parallel. The for...of loop awaits properly:
(async () => {
for (const n of [1, 2, 3]) { // ✅ each iteration waits
await wait(10);
console.log('for-of tick', n);
}
console.log('for-of done');
})();
Output:
for-of tick 1
for-of tick 2
for-of tick 3
for-of doneCommon Mistakes
❌ await in a forEach — the loop doesn't wait; use for...of or Promise.all.
❌ await on a non-promise — it works (the value passes through), but it signals you're
not sure what's async. await on a settled value still costs a microtask.
❌ Treating the function's return as the awaited value:
async function load() {
return fetch('/api/data'); // returns a promise, not data
}
❌ awaiting in a non-async function — SyntaxError. The keyword only exists inside
async bodies (and top-level module code).
Best Practices
✅ await sequential steps, Promise.all independent ones (Lesson 26)
✅ Let errors flow to a try/catch or a caller's .catch — Lesson 27
✅ Return values from the function instead of awaiting the last line
✅ Use for...of when you genuinely need to await inside a loop
❌ Don't await inside forEach/map and expect waiting
❌ Don't mark every function async "just in case" — it changes the return type
Interview Questions
Q1. What does async do to a function?
It makes the function return a promise. Whatever the function returns becomes the promise's value; a thrown error rejects it. Inside,
awaitcan be used.
Q2. What does await do?
It pauses the function until the promise settles, then resumes on the microtask queue with the value — or throws if the promise rejected. It's
.thenwritten as syntax.
Q3. Is await blocking?
No — only the function suspends. The event loop keeps running other tasks and callbacks. The function is rescheduled as a microtask when the promise settles.
Q4. Why doesn't await work inside forEach?
forEachcalls the callback and moves on without awaiting it. Each callback is a separate async function. Usefor...oforPromise.allinstead.
Senior follow-up: Rewrite async/await as .then chains — what's the mapping?
The function body becomes a promise. Each
awaitbecomes a.thenthat resumes the rest of the body with the value. The finalreturnfulfils the outer promise; athrowor a rejectedawaitrejects it. The microtask scheduling is identical either way — it's the same queue from Lesson 23.
Follow-up Questions
Q1. What does await 5 do?
Passes
5through after one microtask — non-promises resolve to themselves. Usually a sign you're awaiting something that's already synchronous.
Q2. Can you use await at the top level of a script?
In a browser module or a Node ESM module, yes — top-level await. In CommonJS scripts, no.
Q3. What's the difference between await p and p.then?
Semantics are the same; the difference is control flow.
awaitsuspends the enclosing function, keeping the rest of the code in the same scope;.thenneeds a new function for each step.
Code Example: Async Sequence with a Parallel Step
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function processUser(id) {
const user = await wait(5).then(() => ({ id, name: 'Sam' })); // sequential
const [posts, friends] = await Promise.all([ // parallel
wait(5).then(() => ['post-1', 'post-2']),
wait(5).then(() => ['alex', 'jordan']),
]);
return { user, posts, friends };
}
processUser(7).then((result) => console.log(result));
Output:
{ user: { id: 7, name: 'Sam' }, posts: [ 'post-1', 'post-2' ], friends: [ 'alex', 'jordan' ] }Performance Notes
- When it matters: sequential
awaits over independent network calls add latency linearly —awaiteach one only when the next step depends on it, and usePromise.allotherwise. - When it doesn't: two or three awaited calls in a handler; the difference is tens of milliseconds, not a bottleneck.
- Awaiting inside a loop (each item needs its own request) is the classic serialization mistake — either accept the serial cost or batch with combinators (Lesson 26).
Debugging Scenarios
| Symptom | Likely cause |
|---|---|
| Function returns a promise instead of the value | async function — log await fn() or .then(...), not fn() |
| Logs print out of order | Forgot await, or awaited a fire-and-forget call |
| "Can only await async" | await used outside an async function (or non-module top level) |
Values are undefined in the next step | A promise-returning call that wasn't awaited, or a handler that didn't return |
| Loop results arrive in random order | forEach/map with async callbacks — they all started together |
Quick Revision Notes
asyncfunction ⇒ always returns a promise; body'sreturn/throwbecomes its settlementawait⇒ pause here, resume as a microtask with the value, or throw on rejectionawaitis.thenas syntax — same queue, same ordering (Lesson 23)- Sequential by default: each
awaitwaits; usePromise.allfor parallel forEachdoes not await — usefor...oforPromise.allawaitis non-blocking: only this function suspends, the event loop keeps running
Cheat Sheet
async function load() {
const data = await fetch('/api/data'); // pause & resume as a microtask
return transform(data); // resolves load()'s promise
}
// equivalent chain
function load() {
return fetch('/api/data')
.then((data) => transform(data));
}Key Takeaways
Recap
asyncmakes a function return a promise;returnfulfils it,throwrejects itawaitpauses the function and resumes on the microtask queue (Lesson 23)awaitis.thenwritten as syntax — neverreturn, and never blocking- Sequential awaits are explicit dependencies; use
Promise.allfor parallel work forEach+asyncis fire-and-forget;for...ofawaits properly- Errors from a rejected
awaitbecome thrown exceptions — the hook into Lesson 27
Check your understanding
Answer each one out loud. Then rate yourself honestly.
0/6What does an async function always return? What settles that promise?
What does await do to the function's execution — and on which queue does it resume?
Rewrite const v = await p; use(v); as an equivalent .then chain.
Why doesn't [1,2,3].forEach(async () => …) wait for each callback? Give two fixes.
Is await blocking? Answer as if the interviewer expects a trick.
Two independent fetch calls: do you await them one after the other? Why not?
What's Next
Lesson 26 — Promise Combinators. Promise.all vs allSettled vs race vs any — a
fast way to check real experience. You'll learn which one to reach for when some requests
may fail.