skip to content

In asynchronous programming, what is the distinction between a future and a promise, and why do many libraries hand out two separate objects for a single asynchronous result?

level: juniorimportance: must knowfreq 62%

answer

  1. Promise writes, future reads
  2. Empty cell filled once
  3. Single assignment, first completion wins
  4. Late observers fire immediately
  5. Not a thread, just a handle

basics

~20 s

A future is the read side of a result that is not ready yet: you await it or attach a continuation. A promise is the write side: the producer completes it once, with a value or an error. Splitting them stops consumers from completing results.

solid answer

~50 s

Both names describe one cell that starts empty and is filled exactly once, but they name opposite ends of it. - **Promise (completer, write side):** kept privately by whoever produces the result. It offers complete(value) and fail(error), and only the first completion counts. - **Future (read side):** handed to callers. It offers *tell me when it is done* — attach a continuation, await it, or block on it — plus a state query. The split is capability separation. If one object carried both halves, any caller could complete somebody else's result, and two observers could race to fill it. Returning a read-only future makes the contract explicit: you may observe, not produce. A future is a synchronization handle, not a thread and not the work itself. Holding one says nothing about whether the work is running, finished, or even started — only that you will eventually learn the outcome.

code

text · 10 lines
text
function fetchUser(id):
    p = new Promise()          # write side, stays local
    startBackgroundWork(
        onSuccess = v -> p.complete(v),
        onFailure = e -> p.fail(e))
    return p.future()          # read side, given to callers

# caller can observe, cannot complete
f = fetchUser(7)
f.onComplete(result -> ...)

go deeper

for a junior

Be able to state the split cleanly: promise completes, future observes, exactly one outcome. Mentioning that a future is not a thread already puts you ahead.

for a middle

Add the state machine and the single-assignment rule, and explain why the write side stays private in a well-designed API.

for a senior

Talk about the failure paths: promises that are never completed, timeout-versus-work completion races, and disposing a value that arrives after nobody is listening.

for a principal

Frame it as capability design — a returned future is a read-only contract, and handing a promise inward is a deliberate inversion that shows who owns the outcome.

## The problem being solved When a computation cannot finish right away — a network call, a disk read, work handed to another worker — the caller needs something to hold in the meantime. A **future** is that something: a handle to a value that does not exist yet, plus a way to be told when it does. Other names for the same idea are *deferred*, *task*, or *awaitable*. ## One cell, two halves Underneath, a future is a tiny state machine. It starts **pending** and moves exactly once into a terminal state: *completed with a value* or *completed with an error*. Two capabilities surround that cell: 1. Somebody must be able to **write** the outcome. That capability is the **promise** (also called completer, or deferred). 2. Somebody must be able to **read** the outcome. That capability is the **future**. Libraries differ in packaging. Some create a promise object and ask you for its future view; some create a single object but hand the completion functions only to the code that produces the result. Conceptually it is always the same: one write end, many read ends. ## Why separate the halves - **Least privilege.** A function that returns a read-only future cannot have its result forged or short-circuited by a caller, and a caller cannot accidentally complete a result that another observer is waiting on. - **Single assignment is enforceable.** With one designated writer, the invariant *exactly one outcome, forever* is easy to keep. If everyone could write, the last writer would win nondeterministically. - **The API says what it means.** A returned future documents *I will tell you the outcome later*; passing a promise into a function documents the opposite — *you produce the outcome, I will read it*. That direction of data flow is otherwise invisible in a signature. ## Completion semantics you are expected to know - **Single assignment.** The first completion wins; later attempts are rejected or silently ignored, depending on the library. This matters when a timeout path and the real work path race to complete the same promise: the loser must not corrupt the outcome. - **Many observers, one outcome.** Every continuation attached to the future sees the same value or the same error. - **Late attachment fires immediately.** If you attach a continuation after completion, it runs at once rather than never. Consumers therefore do not need to register before the work finishes. - **Two terminal channels.** Every consumer has to handle *value* and *error*; a future that can only carry a value is an incomplete abstraction, because the producer can fail. ## What a future is not - It is **not a thread**. No concurrency is implied by the type; a future can be completed by the same thread that created it. - It is **not proof that work started**. Some libraries return eager futures whose work is already in flight; others return lazy handles that start only when observed. You must read the documentation of the specific abstraction rather than assume. - It is **not an excuse to block**. You can usually block a thread until the future completes, but doing that inside async code throws away the benefit and can deadlock when the completer needs the thread you just parked. ## How to use the split in your own designs The classic pattern is: create a promise, keep it private, publish only its future, and make every exit path from your producer complete it exactly once — including the failure and timeout paths. A promise that is never completed is a leak: every consumer waits forever, and in some runtimes nothing ever reclaims the continuation chain. Reviewers look for exactly that: a code path where the promise is neither completed nor failed.

  • What happens if the same promise is completed twice, for example by the worker and by a timeout handler?
    Completion is single assignment: the first one to arrive fixes the outcome and later attempts are rejected or ignored. That is exactly why timeouts are safe to implement this way — the loser of the race cannot overwrite the winner. The one hazard is a resource arriving from the losing path (an open connection or file handle in a value nobody will read), which you must dispose explicitly.
  • Does receiving a future mean the work has already started?
    Not necessarily. Some abstractions are eager — the work is in flight before the future is returned — and some are lazy, starting only when a consumer subscribes or awaits. The type alone does not tell you, so treat it as a documented property of the specific API. It matters because a lazy handle that nobody awaits performs no work at all.

A promise is the box's lid you can close from the inside once; the future is the window others watch through. Anyone can look; only the producer can put something in.

saying these in an interview costs you the question

  • Saying a future runs the computation, as though the handle were the worker or a thread
  • Claiming there is no read/write distinction at all and the two words are simply interchangeable in every model
  • Believing a future can be completed several times, with the latest value winning
  • Assuming a continuation attached after completion never fires
  • Insisting you must block on get() to make any use of a future

context