Concurrency Patterns

Futures & Promises

A handle to a value that hasn't arrived yet — so your code never has to wait for it

A future (or promise) is a placeholder object for a value that isn't ready yet, letting you register callbacks and compose asynchronous work without blocking the calling thread.

  • Statespending → fulfilled / rejected
  • Settlesexactly once (immutable)
  • Callback timingalways async (microtask)
  • Composition.then / all / race / await
  • First shippedMultiLisp, 1985

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

The intuition: an IOU for a value

Imagine you order a coffee and the barista hands you a numbered receipt instead of the drink. You don't stand frozen at the counter — you take the receipt, find a seat, answer an email. The receipt is the coffee, eventually. When your number is called, the value materializes. That receipt is a future, and the barista's commitment to fill it is a promise.

Concretely, a promise is a small object with two parts: a state and an eventual value. It starts pending — no value yet. Some asynchronous work runs elsewhere: a network request, a disk read, a computation on a thread pool. When that work finishes, it settles the promise, either fulfilled with a value or rejected with an error. Crucially, the function that handed you the promise returned immediately, before any of this happened. Your thread never blocked.

The magic is in how you read the result. Instead of polling while (!ready) {}, you register a continuation: "when the value arrives, run this." That callback is the heart of composition — you describe what should happen next without waiting for now.

The state machine and the settle-once rule

Every promise is a tiny three-state machine, and the transitions are one-way:

              resolve(v)
   ┌───────┐ ─────────────▶ ┌────────────┐
   │pending│                │ fulfilled  │  (value = v)
   └───────┘ ─────────────▶ └────────────┘
              reject(e)
                  │
                  ▼
            ┌────────────┐
            │  rejected  │  (reason = e)
            └────────────┘

The single most important property: a promise settles exactly once and is then immutable. The first resolve or reject wins; every subsequent call is ignored (JavaScript) or throws std::future_error (C++). This immutability is what makes promises composable and thread-safe to read: once you've seen a fulfilled promise, it can never become rejected under you, and no data race can change the value mid-read.

Immutability also explains a question that confuses newcomers — what if I subscribe after it already resolved? Your callback still fires. A settled promise simply replays its stored value to every new subscriber. The order of "subscribe" versus "resolve" never matters, which removes an entire category of timing bugs.

One subtle but mandated rule: callbacks never run synchronously inside .then, even if the promise is already settled. They are deferred to the next turn of the event loop — the microtask queue in JavaScript. This guarantees a function is never "sometimes sync, sometimes async" (the bug Isaac Schlueter named "releasing Zalgo"), so the code immediately after .then always runs before the callback.

When to reach for a promise

  • Any single async result — an HTTP call, a file read, a database query, a timer. One operation, one eventual value.
  • Composing dependent steps — "fetch the user, then their orders, then render." Chaining replaces the nested-callback "pyramid of doom."
  • Fan-out / fan-in — fire ten requests and wait for all (Promise.all) or for the fastest (Promise.race).
  • Decoupling producer from consumer — the thread computing a value and the thread reading it never need to meet; the promise is the rendezvous.

Reach for something else when the source produces many values over time, not one — that's a stream or an observable (RxJS, reactive streams), not a promise. A promise resolves once; a stream emits repeatedly. And if you need to cancel in-flight work, note that a bare promise has no cancellation — you need an AbortController or a cancellation token alongside it.

Futures & promises across languages

JavaScript PromiseC++ std::futureJava CompletableFuturePython asyncio.FutureRust FutureScala Future
Read / write splitmerged (one object)future + promisemergedmergedpoll-based (lazy)future + Promise
Blocking getnone (must await).get() blocks.get() blocks.result() blocksnone — driven by executorAwait.result blocks
Non-blocking callback.thennone (need std::async).thenApplyadd_done_callback.await in async fn.map / .flatMap
Eager or lazyeager (runs at creation)eagereagereagerlazy (does nothing until polled)eager
Chaining returnsnew Promisen/anew stagen/anew Futurenew Future
Error channelreject / .catchstored exception.exceptionallyset_exceptionResult<T,E>.recover

The headline distinction is eager vs lazy. In JavaScript, Java, and Scala a promise starts its work the moment it's created — by the time you have the object, the request is already in flight. Rust is the outlier: a Future is inert until an executor polls it, which is why a Rust future you forget to .await does nothing at all, whereas a JS promise you forget to await still runs (and may throw an unhandled rejection).

What the numbers actually say

  • A microtask is cheap but not free. Each .then schedules a microtask; resolving a chain of n promises means n microtask drains. On V8 a microtask dispatch is on the order of tens of nanoseconds, so a 5-stage chain costs roughly 100–300 ns of scheduling overhead — negligible next to the millisecond-scale I/O it's wrapping.
  • Blocking wastes a whole thread. A thread parked in C++ future.get() holds ~1 MB of stack and an OS scheduler slot doing nothing. Serving 10,000 concurrent slow requests with blocking threads needs ~10 GB of stack; the same load on a single-threaded promise/event-loop model needs one thread and a few KB per pending promise. That memory gap is the entire reason Node.js and nginx exist.
  • Promises don't add parallelism — they add concurrency. Promise.all([a, b, c]) on three 100 ms network calls finishes in ~100 ms, not 300 ms, because the calls overlap in flight. But three CPU-bound 100 ms computations still take 300 ms on one thread — promises interleave waiting, not computing.
  • Unhandled rejections leak. A rejected promise with no .catch keeps its error alive and triggers an unhandledRejection event; in Node ≥15 that terminates the process by default.

JavaScript implementation

JavaScript's Promise is native, but building a minimal one yourself is the clearest way to see the state machine. Here is a compact .then-able promise that respects the settle-once and always-async rules:

const PENDING = 0, FULFILLED = 1, REJECTED = 2;

class Tiny {
  constructor(executor) {
    this.state = PENDING;
    this.value = undefined;
    this.cbs = [];                       // queued { onF, onR, resolve, reject }
    const settle = (state, value) => {
      if (this.state !== PENDING) return; // settle-once: ignore extra calls
      this.state = state;
      this.value = value;
      // always async: flush on the microtask queue, never synchronously
      queueMicrotask(() => this.cbs.forEach(c => this._run(c)));
    };
    try { executor(v => settle(FULFILLED, v), e => settle(REJECTED, e)); }
    catch (e) { settle(REJECTED, e); }
  }

  _run({ onF, onR, resolve, reject }) {
    const handler = this.state === FULFILLED ? onF : onR;
    if (typeof handler !== 'function') {           // pass through
      return this.state === FULFILLED ? resolve(this.value) : reject(this.value);
    }
    try { resolve(handler(this.value)); }          // chain: feed the next promise
    catch (e) { reject(e); }
  }

  then(onF, onR) {
    return new Tiny((resolve, reject) => {
      const cb = { onF, onR, resolve, reject };
      if (this.state === PENDING) this.cbs.push(cb);
      else queueMicrotask(() => this._run(cb)); // already settled? still async
    });
  }
  catch(onR) { return this.then(undefined, onR); }
}

// Usage — note the call returns instantly, callback fires later:
new Tiny((res) => setTimeout(() => res(20), 50))
  .then(v => v * 2)        // 40
  .then(v => v + 1)        // 41
  .then(v => console.log(v));
console.log('this prints FIRST'); // proves non-blocking + async callbacks

Two details carry the whole design. First, settle bails out if the state isn't PENDING — that one line enforces immutability. Second, then returns a new promise whose resolution is wired to the handler's return value; that's why chains flow values downstream and a single .catch at the end can absorb an error thrown anywhere upstream.

Python implementation

Python's asyncio exposes futures directly, and the producer/consumer split is explicit. This mirrors the same lifecycle — create pending, resolve elsewhere, await without blocking the loop:

import asyncio

async def main():
    loop = asyncio.get_running_loop()
    fut = loop.create_future()          # pending future (the read end)

    # A producer settles it later — here after a simulated 0.5s of work.
    def producer():
        if not fut.done():              # settle-once guard
            fut.set_result(42)
    loop.call_later(0.5, producer)

    # Non-blocking callback (like .then) — fires when the future settles:
    fut.add_done_callback(lambda f: print("callback got", f.result()))

    print("this prints first — we did not block")
    value = await fut                   # suspend THIS coroutine, not the loop
    print("awaited value:", value)      # 42

asyncio.run(main())

# Compose many: gather is Python's Promise.all
async def fan_out():
    async def fetch(n):
        await asyncio.sleep(0.1)        # overlapping I/O, not 3x serial
        return n * 10
    results = await asyncio.gather(fetch(1), fetch(2), fetch(3))
    print(results)                      # [10, 20, 30] in ~0.1s, not 0.3s

The key line is await fut: it suspends only the current coroutine and hands control back to the event loop, which is free to run other tasks. Nothing blocks. For thread-pool work, loop.run_in_executor returns a future that settles when the pool thread finishes — the same abstraction bridging blocking code into the async world.

Variants and the operators worth knowing

Promise.all vs allSettled vs race vs any. These four combinators cover the common fan-in shapes:

await Promise.all([a, b, c]);        // all results, but rejects on FIRST failure
await Promise.allSettled([a, b, c]); // never rejects; [{status,value|reason}, ...]
await Promise.race([a, b, c]);       // first to SETTLE wins (fulfill OR reject)
await Promise.any([a, b, c]);        // first to FULFILL wins; rejects only if all fail

Deferred. An older idiom (jQuery, Q) that hands you the resolve/reject functions outside the executor, so you can settle the promise from anywhere. ES2024 standardized it as Promise.withResolvers(), which returns { promise, resolve, reject } together.

Cold (lazy) futures. Rust's Future and Scala's IO (Cats Effect) don't start work until driven. This makes them composable as pure descriptions and trivially cancellable — drop the future and nothing leaks. The trade-off is you must remember to run them.

async/await. Not a different primitive — sugar over promises. An async function returns a promise; await p is p.then(...) with the continuation written as the rest of the function body. The bytecode literally suspends and resumes the function around the same microtask machinery.

Promise pipelining. In capability systems like Cap'n Proto and the original E language, you can call a method on the result of a promise before it resolves, batching dependent remote calls into a single round trip — a powerful latency win over the network.

Common bugs and edge cases

  • Forgetting to return inside .then. .then(() => { doAsync(); }) doesn't wait for doAsync — you dropped its promise. Write .then(() => doAsync()) so the chain links to it.
  • Swallowed rejections. A chain without a trailing .catch (or a try/catch around await) silently loses errors and triggers an unhandled-rejection crash in modern Node.
  • Sequential await in a loop. for (const u of urls) await fetch(u) runs requests one at a time. Use await Promise.all(urls.map(fetch)) to overlap them — often a 10× wall-clock win.
  • Assuming .then runs synchronously. Even on an already-resolved promise, the callback is deferred to a microtask. Code written assuming immediate execution breaks subtly.
  • Mixing up all and allSettled. If one of ten requests may fail and you still want the other nine, Promise.all throws everything away on the first rejection — reach for allSettled.
  • No cancellation. A promise can't be cancelled; the underlying work keeps running even after you stop caring. Pair it with an AbortController if you need to abort an in-flight fetch.
  • Returning a promise from an executor expecting it to "unwrap." Resolving a promise with another promise chains them (the spec's "thenable" assimilation), but resolving with a plain object that merely has a then method also triggers assimilation — an accidental footgun if your data object happens to have a field named then.

Frequently asked questions

What's the difference between a future and a promise?

They're two ends of the same channel. The promise is the write end — the producer calls resolve() or reject() on it exactly once. The future is the read end — the consumer reads the value or attaches callbacks but can't set it. JavaScript merges both into one Promise object; C++ std::promise/std::future and Java keep them separate.

Does a promise block the calling thread?

No. Creating a promise and attaching .then returns immediately; the calling thread keeps running. Blocking only happens if you explicitly wait — for example C++ future.get() or Python future.result(), which park the thread until the value arrives. JavaScript has no blocking wait at all; you must use .then or await.

What happens if you resolve a promise twice?

The first resolve or reject wins and freezes the promise forever; every later call is silently ignored in JavaScript. A settled promise is immutable, which is exactly why subscribing after it resolved still delivers the value. In C++, calling set_value on an already-satisfied std::promise throws std::future_error instead.

Why does .then run asynchronously even when the promise is already resolved?

The spec guarantees callbacks always fire on a later turn of the event loop — the microtask queue — never synchronously inside .then. This removes the "releases Zalgo" class of bug where a function is sometimes sync and sometimes async, which makes ordering unpredictable. Your code after .then always runs before the callback.

How is async/await related to promises?

async/await is pure syntactic sugar over promises. An async function always returns a promise, and await pauses the function until a promise settles, then resumes with its value — equivalent to a .then continuation but written as straight-line code. There is no new mechanism underneath; await is .then with the callback hidden.

What is the difference between Promise.all and Promise.race?

Promise.all waits for every promise and resolves with an array of results, but rejects as soon as any one rejects. Promise.race settles as soon as the first promise settles, fulfilled or rejected. Promise.allSettled never short-circuits — it waits for all and reports each outcome; Promise.any resolves on the first success and only rejects if all fail.