Skip to content
Intermediate10 min study

Async and await

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

Question progress0 / 10 completed
Start the lesson
Async and await visual explanation

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.

Think of it like this: await is like ordering at a counter and stepping aside to let other customers be served while you wait for your own order — you're not blocking the whole line, you're just pausing your own turn until your order's ready.

03Examples

JavaScript
// 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.

How to say it out loud: "async/await is syntax built on top of promises — an async function always returns a promise, and await pauses that specific function's execution until the awaited promise settles, without blocking the rest of the program. The most common performance bug is writing two independent awaits back to back, which accidentally makes them run sequentially instead of concurrently — if the two operations don't depend on each other, I'd start both promises first and then await them together with Promise.all, so they run in parallel 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.
JavaScript
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.

JavaScript
// 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)]);
Open question page →
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.

JavaScript
// 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));
Open question page →
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.

  1. Choose whether uploads should be sequential, fully concurrent, or concurrency-limited.
  2. For concurrency, map files to promises and await the aggregate.
  3. Define partial-failure behavior.
  4. 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.

Verify and go deeper