Tags: web-dev concept

Async Models

Date: 2026-08-17


Callbacks, promises, async/await, coroutines and observables are the same problem solved with different ergonomics: how to express “do this when the waiting finishes” without blocking. Knowing they’re one problem makes each new one cheap to learn.


An async model is a way of expressing work that completes later. The underlying mechanism is nearly always the same — register interest, return control, resume on completion. What differs is how the code reads and how errors propagate.

The same operation, four ways

Callback

getUser(id, (err, user) => {
  if (err) return handle(err)
  getOrders(user, (err, orders) => {
    if (err) return handle(err)
    render(orders)
  })
})

Promise

getUser(id)
  .then(getOrders)
  .then(render)
  .catch(handle)

Async / await

try {
  const user = await getUser(id)
  const orders = await getOrders(user)
  render(orders)
} catch (e) { handle(e) }

Observable / stream

from(getUser(id))
  .pipe(switchMap(getOrders))
  .subscribe({ next: render, error: handle })

Identical work. The difference is entirely in error handling and readability — which is not a small thing, because that’s where the bugs are.

What each fixed

CALLBACKS
  the original. Nesting grows rightwards,
  errors are a convention you can forget,
  and a callback can be called twice

PROMISES
  a value representing a future result
  → composable, chainable
  → errors propagate down the chain
  → settles exactly once

ASYNC / AWAIT
  promises with synchronous-looking syntax
  → try/catch works normally
  → loops and conditionals work normally
  ← still promises underneath

OBSERVABLES
  for MANY values over time, not one
  → cancellation, backpressure, operators
  → heavier concept, real payoff for
    streams and event sequences

Promises are one value; observables are many. That’s the actual dividing line — a fetch is a promise, a websocket is a stream.

The distinction that catches people

A promise represents work already started. Creating it begins the operation; await only waits for it. The promise state machine itself is in Promises and Async Await.

// SEQUENTIAL — 300ms
const a = await fetchA()
const b = await fetchB()
 
// CONCURRENT — ~100ms
const pa = fetchA()      // both start
const pb = fetchB()      // immediately
const a = await pa
const b = await pb
 
// same thing, idiomatic
const [a, b] = await Promise.all([
  fetchA(), fetchB()
])

await in a loop is the trap, and it’s extremely common — n sequential round trips where one batched call or a Promise.all would do — Concurrency and Parallelism, Latency and Bandwidth.

Combinators worth knowing

Promise.all         all succeed, or reject
                    on the FIRST failure
Promise.allSettled  never rejects; returns
                    status for each
Promise.race        first to settle, win
                    or lose
Promise.any         first to SUCCEED

allSettled is usually what you want when fetching independent things — one failed recommendation call shouldn’t take the whole page down.

Where it goes wrong

  • Unhandled rejections. A promise with no .catch and no enclosing try fails silently in some environments and crashes the process in others. Always terminate a chain
  • Forgetting await. Returns a promise where a value was expected — often no error, just [object Promise] in the output or a condition that’s always truthy
  • forEach with an async callback doesn’t wait. It returns immediately, having started n operations nobody is tracking. Use for...of with await, or Promise.all(map(...))
  • Fire-and-forget error loss. An async function called without await or .catch discards its failure
  • No cancellation in promises. A promise cannot be cancelled — only ignored. Use AbortController on the underlying operation, or the response arrives after the component that wanted it has gone — Race Conditions

The one thing to carry

Async is not concurrency and it is not parallelism. It’s a way of not blocking while something else takes time — The Event Loop is the machinery underneath, and every model above is a different syntax over the same queue.