Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 19

async/await

This chapter assumes you are already fluent with Promise semantics from the previous chapter: chaining, error propagation, rejection, `Promise.all`/`allSettled`/`race`/`any`, and that you know the event loop model — synchronous run-to-completion, microtask queue, macrotask queue. We treat `async/awa
Level: AdvancedLength: In depth4 min read

Read at another depth

async/await

Prerequisites assumed

This chapter assumes you are already fluent with Promise semantics from the previous chapter: chaining, error propagation, rejection, Promise.all/allSettled/race/any, and that you know the event loop model — synchronous run-to-completion, microtask queue, macrotask queue. We treat async/await not as sugar to be ignored but as a control-flow mechanism with observable, sometimes surprising, semantics. Where behavior differs from raw promise chaining, we surface the difference.

The core model

An async function is a function that always returns a promise. Inside, await suspends that function, queues the rest of it as a continuation on the promise being awaited (a microtask), and returns control to the caller. Crucially, the caller of an async function keeps running the moment the promise is returned — the suspension is local.

Code example in javascript
async function main() {
  const value = await Promise.resolve(42);
  console.log(value); // 42
}
main();
console.log('main() has returned a promise; program continues');

main() is invoked synchronously; everything up to the first await runs eagerly. Only after hitting await does the function yield. This is why the first segment of an async function is sometimes called the synchronous prefix — errors thrown before the first await become a synchronous throw, not a rejection:

Code example in javascript
async function bad() {
  throw new Error('sync prefix');
}
bad(); // returns a rejected promise — the throw is converted

Note the subtlety: even the "synchronous throw" above is captured by the async machinery into a rejection, because an async function cannot throw synchronously. But compare:

Code example in javascript
function throwsSync() {
  throw new Error('now');
}
const p = Promise.resolve().then(() => throwsSync()); // rejection
throwsSync(); // synchronous throw — crashes the call stack

Await semantics in detail

await expr desugars roughly to: take the value, if it is not a thenable wrap it in Promise.resolve, register a then continuation whose body is the remainder of the function, then return. await on a non-promise value still yields to the microtask queue — an await of a plain number is one microtask tick, not zero.

That last point is a real behavioral difference from promise chains. In a .then chain there is no forced pause between steps; each await inserts a microtask boundary. In a tight loop this matters:

Code example in javascript
async function spin() {
  for (let i = 0; i < 1_000_000; i++) {
    await undefined; // 1M microtask ticks — measurable overhead
  }
}

V8 and other engines optimize heavily here (await is specialized in the bytecode), but if the awaited value is already resolved you are still paying queue hops. Batch what you can.

Execution order: the interleaving hazard

Sequential awaits inside one function are easy. The hazards come from concurrent async functions interleaving their await points. Consider shared mutable state:

Code example in javascript
let balance = 100;
async function withdraw(amount) {
  if (balance >= amount) {           // check
    await dbLog(amount);             // suspension point
    balance -= amount;               // act
  }
}
await Promise.all([withdraw(80), withdraw(80)]);
// balance can end at 20: both passed the check before either acted

This is a check-then-act race. There is no lock primitive; you either avoid the interleaving (single-threaded ordering), restructure with an async queue/mutex, or perform the mutation atomically before any await.

Error handling

try/catch around await catches rejections from that await, including errors thrown by code after it in the same block. Design decisions to make deliberately:

  1. Per-await catch vs. block catch. A block-wide try conflates which operation failed. await promise.catch(handler) scopes recovery to one call.
  2. Errors crossing await boundaries lose no stack frames if the async context chains them, but the stack of who awaited only exists while suspended. Use async-stack-traces-style runtimes (all modern engines) and be aware stack traces still truncate at the boundary of fire-and-forget calls.
  3. Fire-and-forget rejects silently. An async function invoked without await produces a promise nobody handles → unhandled rejection. In Node this crashes the process by default since v15; in browsers it logs and can be hooked via unhandledrejection.
Code example in javascript
async function risky() {
  try {
    const a = await stepA();       // failure here...
    const b = await stepB(a);      // ...and here, same catch
    return b;
  } catch (err) {
    // cannot tell which step failed without inspection
    throw new Error('pipeline failed', { cause: err });
  }
}

Prefer wrapping errors with cause (ES2022) to preserve the chain rather than flattening it.

Concurrency patterns

The default of sequential awaits is safe but slow. Common patterns:

PatternCode shapeWhen to use
Parallel`await Promise.all([f(), g()])`Independent work, fail-fast acceptable
All-settled`await Promise.allSettled([...])`You want every outcome, partial success fine
Limited concurrencysemaphore / chunked `all`Bounded resources (APIs, sockets)
Sequential`for...of` with `await`Order matters, or throttling via ordering
Fire-and-collectkick off promises, `await` laterStart early, consume later
Code example in javascript
async function limitedAll(items, limit, fn) {
  const results = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const i = next++;
      results[i] = await fn(items[i]);
    }
  }
  await Promise.all(Array.from({ length: limit }, worker));
  return results;
}

A trap: Promise.all with an array you build via map(fn) — if fn is async, all the work has already started before all is called. To sequence instead, for (const x of items) await fn(x), or use the worker-pool above.

Return-await trade-offs

  • return promise vs return await promise inside try: the latter is required for the catch to see the rejection. Outside try, return promise skips one microtask hop.
  • ESLint's no-return-await is right in general, wrong in try blocks.
  • for await...of consumes async iterables (next chapter's concern); remember it serializes by design.

Cancellation and timeouts

await gives you no cancellation. A promise you stop awaiting still runs to completion; its resources still leak. Mitigations:

Code example in javascript
function withTimeout(p, ms) {
  return Promise.race([
    p,
    new Promise((_, rej) =>
      setTimeout(() => rej(new Error('timeout')), ms)),
  ]);
}

But the underlying work is not cancelled — for that you need AbortSignal at the API level (e.g., fetch(..., { signal })) and cooperation from the task itself. Design async APIs to accept a signal; don't rely on abandoning promises.

callout warning title: Top-promise awaits do not run in parallel

const x = await f(); const y = await g(); always sequences. If f and g are independent, start both first: const px = f(), py = g(); const x = await px, y = await py;. Sequential-by-default is the single most common async performance bug.

Diagram

Flow diagram showing caller calling an async function, the function running synchronously to its first await, returning a promise to the caller who continues, and the function's remainder resuming later from the microtask queue.
100%
Async function suspension: the caller continues while the awaited continuation queues as a microtask.
Show diagram source
sequenceDiagram
    participant C as Caller
    participant F as async fn
    participant Q as Microtask queue
    C->>F: call
    F->>F: sync prefix runs
    F->>Q: register continuation on awaited promise
    F-->>C: return pending promise
    C->>C: continues immediately
    Note over Q: ...promise resolves...
    Q->>F: resume after await
    F->>F: rest of function
    F->>Q: resolve returned promise

Exercise

Exercise

In the interleaving example, why does withdraw(80) twice not produce a rejected second call or a negative balance, and what invariant is broken instead?

Show answer

Neither withdrawal throws — each independently passes the balance >= amount check while the other is suspended at await dbLog. The invariant broken is "balance after withdrawal reflects all prior withdrawals": both subtract from the stale value of 100, so 160 was withdrawn from a 100 balance. The check and the mutation are not atomic across the await boundary.

Exercise

You replace return await p; inside a try with return p; and your catch stops firing for p's rejection. Explain precisely why.

Show answer

return p hands the pending promise straight back to the caller; the async function is no longer involved in its fate, so the rejection settles that returned promise rather than throwing into the current try block. return await p makes the function await the result, so a rejection surfaces as a throw at the await site and is caught by the surrounding catch.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: async/await

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that