Futures and Promises
A future is a placeholder for a value that will be available later. A promise is the producer side that fulfils the future. The pair allows starting work asynchronously and consuming the result with backpressure (the consumer waits if not ready). Foundational primitive in async programming.
Diagram
What it is
A future is a value that is not yet known. The caller receives a handle now; the value arrives later. A promise is the writer side: it fulfils the future with a value or an error.
In some languages they are one type (JavaScript Promise, Scala Future, Rust Future). In others they are split (Java Future and CompletableFuture, C++ std::future and std::promise). The mental model is the same: a placeholder for an asynchronous result.
A picture of a future's life
A future is a placeholder for a value that doesn't exist yet. It has a small state machine, shown in the diagram above:
- PENDING: created, work is in flight.
- COMPLETED: work succeeded; the future holds a value.
- FAILED: work threw; the future holds an exception.
- CANCELLED: cancel() was called. The future is marked cancelled, but the underlying work may not actually stop.
await or .get() blocks until the future leaves PENDING. The future itself is just bookkeeping: a slot to hold either a value, an exception, or a cancelled marker. The work happens elsewhere (a thread pool, an event loop, an OS task). The future is the way to talk about that work without waiting for it.
This is one of the foundational primitives of async programming. Most async APIs return futures. Most async composition operates on futures.
What it provides
Asynchronous execution. Start work, get a future immediately, keep doing other things, consume the result when needed.
Composition. Chain futures (thenApply, then, await ... + await ...). Wait for all (allOf, gather). Wait for any (anyOf, wait FIRST_COMPLETED). The combinators let one express "do these N things in parallel and combine" without manual lock-and-counter bookkeeping.
Backpressure. Calling .get() or await blocks the consumer until the value is ready. The consumer cannot run ahead of the producer.
Error propagation. Exceptions in the producer become exceptions when the consumer awaits. No callback-style error handling in every stage.
What it does NOT provide
True cancellation. Cancelling a future moves it to CANCELLED, but it does not necessarily stop the underlying work. The producer has to cooperate by checking a cancellation flag or responding to interrupts. This is the most common surprise: future.cancel() returns true while the network call that was started keeps running in the background.
Synchronisation of shared state. Two futures whose work touches the same mutable variable still race. Futures coordinate the result, not access to shared state. Locks or atomics are still required for that.
The composition operations
| Operation | What it does |
|---|---|
then / thenApply / map | Transform the result with a function |
thenCompose / flatMap / await ... await | Sequence: feed result into another future |
allOf / gather | Wait for all, return all results |
anyOf / wait FIRST_COMPLETED | Wait for first, return that result |
exceptionally / catch / try await | Recover from an error |
whenComplete / finally | Run something either way |
These compose. A typical async pipeline strings several together:
fetchUser(id)
.thenCompose(user -> fetchProfile(user.id))
.thenApply(profile -> render(profile))
.exceptionally(ex -> defaultPage())
When futures are not the right tool
For ongoing streams of values, use a stream/observable abstraction (RxJava, Reactor, asyncio streams). Futures are one-shot; trying to model a stream as a chain of single futures gets unwieldy.
For coordinated parallel work that needs cancellation on first error, use structured concurrency (Java's StructuredTaskScope, asyncio's TaskGroup, Go's errgroup). Futures + manual cancellation is the older, more error-prone approach.
For long-running background work that has no consumer waiting on the result, use a fire-and-forget goroutine/thread/task. A future that never gets awaited is wasted bookkeeping.
Implementations
thenApply transforms the result. thenCompose flattens (avoids Future<Future<T>>). exceptionally is the error path. The chain runs each stage on whatever pool the previous stage finished on, unless an explicit executor is specified.
1 import java.util.concurrent.CompletableFuture;
2
3 CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.fetch(id));
4
5 CompletableFuture<String> nameFuture = userFuture
6 .thenApply(User::getName) // transform value
7 .thenApply(String::toUpperCase)
8 .exceptionally(ex -> "anonymous"); // recover from error
9
10 CompletableFuture<Profile> profileFuture = userFuture
11 .thenCompose(user -> profileService.fetchAsync(user.getId())); // flatMap
12
13 // Wait for the result (only at the boundary, not inside async code)
14 String name = nameFuture.get();allOf: wait for all to complete (success or failure). anyOf: return as soon as one completes. Used for fan-out: hit N services concurrently, wait for all (or first).
1 CompletableFuture<User> u = CompletableFuture.supplyAsync(() -> fetchUser(id));
2 CompletableFuture<Cart> c = CompletableFuture.supplyAsync(() -> fetchCart(id));
3 CompletableFuture<Recs> r = CompletableFuture.supplyAsync(() -> fetchRecs(id));
4
5 // Wait for all three
6 CompletableFuture<Void> all = CompletableFuture.allOf(u, c, r);
7 all.join(); // block until done
8 // Now safe to call .get() on each since they're all complete
9
10 // Wait for first
11 CompletableFuture<Object> first = CompletableFuture.anyOf(u, c, r);
12 Object firstResult = first.join();Key points
- •Future = consumer-side handle (read result, wait, attach callbacks). Promise = producer-side handle (set value or error).
- •Many languages have only one type that does both jobs: CompletableFuture (Java), Future (Python), Promise (JS), Future (Rust).
- •Composition: chain futures (then/map/flatMap), wait for all (allOf/gather), wait for any (anyOf).
- •Cancellation is the hard part. Many implementations support cancelling the future, but stopping the actual work depends on the producer cooperating.
- •Don't block in a future-returning function. The whole point is async; calling .get() inside an async function defeats the purpose.
Follow-up questions
▸What is the difference between Future and Promise?
▸How is a future cancelled?
▸Why can chaining futures with then/map be cleaner than awaiting each?
▸What is a 'completed' future?
Gotchas
- !Calling .get() / await inside async code defeats the purpose; chain instead
- !Cancellation marks the future cancelled but may not stop the underlying work
- !Java CompletableFuture's default executor (ForkJoinPool.commonPool) is shared; specify an executor for I/O-bound chains
- !Composing futures with shared mutable state still needs synchronisation
- !exceptionally / catch resets the error chain; subsequent stages run on the recovered value, not the error