Skip to content
RungsySign in

async / await

Promises that read like ordinary code — plus the parallelism people accidentally throw away.

30 minDifficulty 2/5language · asyncAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.

Before this

Why this exists

Nested `.then()` and `.catch()` chains solved callback hell, but they fractured control flow into scattered closures and made structured error handling tedious. `async`/`await` flattens asynchronous logic into imperative code that looks synchronous, but developers routinely introduce massive performance bottlenecks by sequentially awaiting independent tasks instead of executing them in parallel.

The mental model

Think of an `async` function as an **automatic bookmark system**. When execution hits `await`, the function places a bookmark at that line, releases the main JavaScript thread to handle other tasks, and returns an unresolved Promise. As soon as the awaited operation resolves, the engine resumes execution from the bookmark.

How it works

Syntax over Promises

An `async` function always returns a `Promise`. If you return a primitive like `42`, the runtime implicitly wraps it in `Promise.resolve(42)`. Inside an `async` function, `await` pauses local execution until the targeted `Promise` settles, unwrapping its resolved value or throwing its rejection as a catchable exception.

Unified Error Handling

Before `async`/`await`, mixing synchronous exceptions with asynchronous rejections required chaining `.catch()` and wrapping initial blocks in `try/catch`. With `await`, both synchronous runtime throws and asynchronous Promise rejections are captured by standard `try { ... } catch (err) { ... }` blocks.

The Sequential Await Antipattern

Writing `const user = await fetchUser(); const posts = await fetchPosts();` creates an accidental waterfall: `fetchPosts` does not initiate until `fetchUser` finishes. When tasks are independent, initiate them simultaneously and await them together using `Promise.all([fetchUser(), fetchPosts()])`.

The mechanism

When an `async` function calls `await <expr>`, the engine evaluates `<expr>` and converts it to a Promise via `Promise.resolve()`. It then attaches the remainder of the async function as a microtask callback and suspends the function frame, returning control to the caller. When the microtask runs on Promise resolution, the engine restores the function frame on the call stack and resumes execution.

sequenceDiagram
  autonumber
  participant Main as Main Call Stack
  participant AsyncFn as Async Function
  participant Micro as Microtask Queue
  
  Main->>AsyncFn: Invokes async function
  AsyncFn->>Main: Reaches `await promise`, yields control
  Note over Main: Executes other synchronous code
  Note over Micro: Promise resolves, queues continuation
  Main->>AsyncFn: Microtask runs, restores state after `await`
  AsyncFn->>Main: Function finishes and resolves outer Promise
Diagram source for async / await.

What people get wrong

Using `await` blocks the main thread like a synchronous `while` loop.
`await` is non-blocking to the browser or Node.js runtime; it only pauses execution inside that specific async function frame. The engine frees the call stack immediately upon encountering `await`, allowing the event loop to process user input, rendering, and other asynchronous events while waiting.
Using `Array.prototype.forEach` with an `async` callback runs items in serial order.
`forEach` is not Promise-aware and will fire all callbacks concurrently without waiting for them to complete. `forEach` ignores the return value of its callback; use a standard `for...of` loop for sequential iteration or `Promise.all(arr.map(...))` for concurrent execution.

When not to use it

You need to process a high-frequency stream of events (e.g., mouse moves, WebSocket messages) over time without holding state suspended.
Use Observables (like RxJS) or Async Iterators (`for await...of`), rather than standard `async`/`await` chains.
You have multiple independent promises where one failing should not abort the others.
Use `Promise.allSettled()` instead of a series of raw `await` statements or `Promise.all()`.

Terms

Syntactic Sugar
Syntax within a programming language designed to make things easier to read or express, without adding new underlying runtime capabilities.
Microtask Queue
A high-priority queue in the JavaScript event loop dedicated to processing callbacks from Promises and `await` continuations before the next rendering or macro task.
Request Waterfall
A performance antipattern where network requests execute sequentially one after another, despite having no data dependencies between them.

In an interview

What happens under the hood when a function uses `async` and `await`?

  • Explain that `async/await` is syntactic sugar built on top of Promises and Generators.
  • Mention that `await` pauses execution of the local function context, not the JavaScript thread.
  • Highlight that resume steps are queued onto the Microtask Queue once the Promise settles.

How do you handle error handling with `async`/`await` compared to raw Promises?

  • Explain the use of standard `try/catch/finally` blocks.
  • Mention that `try/catch` catches both synchronous errors and rejected Promises within the scope.
  • Discuss unhandled rejection risks if an `async` function is invoked without `await` or `.catch()`.

Can you recall it?

What is the key performance hazard when using `async`/`await` across multiple independent network calls, and how do you resolve it?

Keep track of this

Add The JavaScript Language to your map and Rungsy will schedule reviews so you actually remember it.