Async and await
Write structured asynchronous flows while preserving concurrency, failure semantics, cleanup, and response ordering.

WHAT YOU WILL BE ABLE TO DO
Learning outcomes
- Explain async and await in plain language.
- Connect the behavior to the underlying browser or framework model.
- Implement the core pattern and reason through edge cases.
- Answer common follow-ups without relying on memorized phrases.
01Overview
async/await is syntax over promises: an async function always returns a promise, and await suspends that function's execution — not the thread — until the awaited promise settles, then resumes with the value or throws the rejection.
02Mental model
try/catch around awaited calls replaces .catch chaining for readability, but the underlying rejection semantics are unchanged.
03Examples
// Accidentally sequential — second fetch waits for the firstconst user = await fetchUser(id);const posts = await fetchPosts(id);
// Parallel — both start immediatelyconst [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);04Check understanding
Two independent awaits in a row that don't depend on each other's result — what's the fix?
Start both requests before awaiting either, typically with Promise.all, so they run concurrently instead of one after the other.
DDConcept deep dives
Deep dive 1
Await is promise composition with structured syntax
An async function begins synchronously until it reaches an await. Await converts its operand through promise resolution semantics, suspends this function's evaluation, and allows the stack to unwind. When settlement occurs, the continuation runs through the promise job mechanism. The thread is free; only this async flow is paused.
- An async function always returns a promise, even without an await.
- Throwing before or after await rejects the returned promise.
- try/catch handles awaited rejection using ordinary structured control flow.
Deep dive 2
Concurrency depends on when work starts
Writing two awaits one after another starts the second operation only after the first finishes if each promise is created at its await expression. When operations are independent, create both promises first and await an aggregate. Sequential code is correct when a result is required for the next request or deliberate backpressure is needed.
- Concurrency is not the same as parallel JavaScript execution.
- Launching thousands of requests at once can exhaust connections or services.
- A concurrency limiter combines throughput with resource bounds.
const profilePromise = fetchProfile(id);const activityPromise = fetchActivity(id);
const [profile, activity] = await Promise.all([ profilePromise, activityPromise,]);Both operations start before the first await suspends execution, so total latency is closer to the slower request instead of the sum of both latencies.
Deep dive 3
Error boundaries should preserve meaning
Catch an error where the layer can recover, add domain context, translate it, or present it. A catch that logs and returns undefined silently changes the function's contract and moves the crash elsewhere. Cleanup belongs in finally, while cancellation should be distinguished from genuine failure so users do not see obsolete requests as errors.
- Check HTTP response status because fetch network success does not imply application success.
- Preserve an error cause when wrapping failures.
- Avoid floating promises: await, return, or deliberately handle every started operation.
QAInterview questions and model answers
Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.
Intermediate · Conceptual · 1 min · Question 1What does an async function return?Open model answer
Model answer
It always returns a promise. Returning a normal value fulfills that promise; throwing rejects it. Returning another promise causes the outer promise to adopt its state.
Open question page →Intermediate · Conceptual · 1 min · Question 2What does await do?Open model answer
Model answer
Await evaluates an expression, converts it through Promise resolution semantics, suspends the async function, and schedules its continuation after settlement. It does not block the JavaScript thread or the whole program.
Open question page →Intermediate · Coding · 1 min · Question 3How do you avoid accidental sequential requests?Open model answer
Model answer
Create independent promises before awaiting them, then await them together with the combinator matching the failure semantics. Sequential await is correct only when later work depends on an earlier result or deliberate rate limiting is required.
// Slow — second request waits for the first to finishconst user = await getUser(id);const posts = await getPosts(id);
// Fast — both start now, then we wait onceconst [user, posts] = await Promise.all([getUser(id), getPosts(id)]);Intermediate · Conceptual · 1 min · Question 4Where should errors be caught?Open model answer
Model answer
Catch at a boundary that can add context, recover, translate the error into domain behavior, or present it. Catching only to log and silently continue often corrupts control flow; rethrow when the current layer cannot resolve the failure.
Open question page →Intermediate · Coding · 1 min · Question 5What is the danger of async callbacks in forEach?Open model answer
Model answer
forEach ignores callback return values, so it does not await the promises and the surrounding function finishes early. Use for...of for sequential work or map plus Promise.all for concurrent work.
// Bug: "done" logs before any upload finishesfiles.forEach(async (f) => { await upload(f); });console.log('done');
// Sequential:for (const f of files) await upload(f);// Concurrent:await Promise.all(files.map(upload));Intermediate · Conceptual · 1 min · Question 6How does finally behave with async code?Open model answer
Model answer
A finally block runs whether the try completes or throws and can itself await cleanup. If finally throws or returns a rejected promise, that new failure replaces the earlier completion, so cleanup code should be carefully designed.
Open question page →Advanced · Conceptual · 1 min · Question 7Can await be used at module top level?Open model answer
Model answer
ES modules can use top-level await in supporting environments, but it delays evaluation of dependent modules and can complicate startup waterfalls and cycles.
Open question page →Advanced · Conceptual · 1 min · Question 8How do you preserve a stack or cause when translating errors?Open model answer
Model answer
Throw a domain error with the original error in its cause property, adding useful operation context without discarding the underlying diagnostic information.
Open question page →Intermediate · Conceptual · 1 min · Question 9What happens when a catch block returns a value?Open model answer
Model answer
The async function continues successfully and its returned promise can fulfill with that value. This is recovery, so it should happen only when the fallback satisfies the contract.
Open question page →Advanced · Conceptual · 1 min · Question 10How do you limit concurrent async work?Open model answer
Model answer
Maintain a bounded number of active operations, starting another when one settles. This protects memory, connections, and services while preserving useful throughput.
Open question page →SCScenario questions
Scenario 1
A batch uploader uses files.forEach(async file => await upload(file)) and shows Complete immediately. Correct it.
- Choose whether uploads should be sequential, fully concurrent, or concurrency-limited.
- For concurrency, map files to promises and await the aggregate.
- Define partial-failure behavior.
- Add cancellation and progress based on settled uploads.
Reveal worked answer
For a small batch I would use await Promise.all(files.map(upload)) if any failure should fail the batch, or allSettled for per-file reporting. For large batches I would use a concurrency pool rather than launching everything. Completion is derived from settled operations, not from forEach returning.