Future / Promise — Junior Level¶
Source: Baker & Hewitt (1977, futures) · Doug Lea, Concurrent Programming in Java ·
java.util.concurrent/CompletableFutureCategory: Concurrency — "Patterns for coordinating work across threads, cores, and machines."
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Real-World Analogies
- Mental Models
- Pros & Cons
- Use Cases
- Code Examples
- Coding Patterns
- Clean Code
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Tricky Points
- Test Yourself
- Tricky Questions
- Cheat Sheet
- Summary
- What You Can Build
- Further Reading
- Related Topics
- Diagrams & Visual Aids
Introduction¶
Focus: What is it? and How to use it?
A Future is a read-only placeholder for a value that does not exist yet. You ask a system to compute something asynchronously, and instead of blocking until the answer arrives, you immediately receive a handle — the Future — that you can hold, pass around, and later query: "are you done? what's the result?"
A Promise is the other half of that handle: the writable producer side. Whoever runs the computation holds the Promise and, when the work finishes, completes it — supplying either a value (success) or an exception (failure). Completing the Promise is what makes the Future "resolve."
The whole point is separation of two concerns that callbacks tangle together:
- Starting an asynchronous computation (kick off a download, a query, a CPU-heavy calculation).
- Consuming its result (do something with the answer once it's ready).
A Future hands you a token for "the answer, eventually" so the rest of your program can keep moving. When you finally need the value, you either block and wait, or — far better — attach a continuation ("when this resolves, run that next").
// supplyAsync returns a Future for a value computed on another thread.
CompletableFuture<Integer> priceFuture =
CompletableFuture.supplyAsync(() -> fetchPrice("AAPL"));
// We did NOT block here. priceFuture is a placeholder.
System.out.println("Request sent, doing other work...");
// Later, when we actually need the number:
int price = priceFuture.get(); // blocks ONLY now, if not yet ready
The Future / Promise pattern is the foundation of nearly every modern async API: JavaScript Promise, Scala Future/Promise, C++ std::future/std::promise, Rust Future, C# Task. Learn it once here and you read all of them.
Prerequisites¶
You should be comfortable with:
- Threads — a Future's value is usually produced on a different thread than the one consuming it.
- Exceptions — a Future can complete with a value or an exception; both are first-class outcomes.
- Callbacks / lambdas — composition attaches functions to run "when ready."
- Thread Pool — Futures don't conjure threads; the work runs on an executor.
If "the result isn't ready yet, here's a ticket for it" makes sense to you, you already have the core intuition.
Glossary¶
| Term | Meaning |
|---|---|
| Future | Read-only handle to a value that will exist later. You can query/await/compose it, but not set it. |
| Promise | Writable handle to the same slot. The producer completes it with a value or an error. |
| Complete / resolve / fulfill | Set the Future's result to a value. Transitions it from pending to done. |
| Reject / completeExceptionally | Set the Future's result to an error. Also a terminal "done" state. |
| Pending | The Future has no result yet. |
| Settled | The Future is done — either fulfilled or rejected. Terminal and immutable. |
get() / await | Block the calling thread until the Future settles, then return the value (or throw). |
| Continuation / callback | A function attached to run when the Future settles (thenApply, .then, map). |
| Eager future | Computation starts immediately when the Future is created (Java, JS). |
| Lazy future | Computation starts only when the Future is driven (polled/awaited), e.g. Rust. |
| Composition | Building a new Future from existing ones without blocking (thenCompose, thenCombine). |
Core Concepts¶
1. The read side and the write side are different objects (conceptually)¶
This is the single most clarifying idea. A Future is read-only: consumers can only observe. A Promise is write-once: the producer completes it exactly once. In Scala and C++ these are literally two types. In Java's CompletableFuture they are merged into one class — but the methods split cleanly:
- Read side:
get(),join(),thenApply,thenCompose,whenComplete. - Write side:
complete(value),completeExceptionally(ex).
CompletableFuture<String> promise = new CompletableFuture<>(); // unsettled
// Hand the READ side to a consumer:
promise.thenAccept(name -> System.out.println("Hello " + name));
// Elsewhere, the producer uses the WRITE side:
promise.complete("Ada"); // now the consumer's callback fires
2. A Future has exactly three states¶
PENDING → FULFILLED or PENDING → REJECTED. Once settled, it is immutable — a second complete() is ignored (returns false). This immutability is what makes Futures safe to share across threads.
3. Don't block if you can compose¶
The beginner instinct is future.get(). That works but throws away the benefit: the calling thread sits idle. Composition lets you describe what happens next without blocking:
CompletableFuture<Integer> lengthFuture =
fetchUsername() // CompletableFuture<String>
.thenApply(String::length); // CompletableFuture<Integer>, no blocking
4. Exceptions flow through the chain¶
If the computation throws, the Future is rejected. Downstream thenApply stages are skipped, and the exception is delivered to the nearest exceptionally / handle / whenComplete — much like a synchronous try/catch, but across threads.
Real-World Analogies¶
- Restaurant buzzer. You order, and instead of standing at the counter (blocking), you get a buzzer (the Future). You sit down and chat (do other work). When food is ready, the buzzer flashes (the Promise is completed) and you go collect it.
- Coat-check ticket. The ticket (Future) is a claim on your coat. The attendant (producer) holds the actual coat and "completes" the claim when you return. You can hand the ticket to a friend — anyone with it can claim the result.
- Tracking number. When you order online you immediately get a tracking number (Future) even though the package doesn't exist on your doorstep yet. You can poll it (
isDone), wait at the door (get), or set up a doorbell notification (thenAccept). - A blank check that someone else fills in. You hand out the check (Future); the payer (Promise holder) writes the amount exactly once.
Mental Models¶
A Future is a box that is empty now and will contain exactly one thing later — a value or an error.
- Think of
CompletableFuture<T>as a mailbox with one slot. The producer drops in one letter (value or error). Readers either wait at the mailbox (get) or leave a note "forward whatever arrives to me" (thenApply). - Think of composition as building a pipeline of empty boxes wired together: when box A fills, its content flows into the function that fills box B, and so on. No thread blocks while the boxes are empty.
- Think of the executor as the kitchen: the Future is your order ticket, but someone in the kitchen (a thread-pool worker) actually cooks. If the kitchen is understaffed, your ticket waits — Futures don't add cooks.
Pros & Cons¶
| ✓ Pros | ✗ Cons |
|---|---|
| Decouples starting work from consuming its result | Easy to accidentally block with get(), killing the benefit |
| Composition avoids deeply nested callbacks ("callback hell") | A rejected Future nobody observes silently swallows the exception |
Exceptions propagate through the chain like try/catch | Which thread runs your callback is non-obvious (thenApply vs thenApplyAsync) |
| One uniform abstraction across the whole language ecosystem | Cancellation is advisory, not guaranteed to stop the work |
Naturally enables parallelism (fan-out, then allOf) | Debugging async stack traces is harder than synchronous ones |
Use Cases¶
- Network/IO calls — fire an HTTP request, get a Future, continue rendering the page.
- Fan-out / fan-in — call three microservices in parallel, combine the three Futures into one result.
- CPU-bound offloading — push a heavy computation onto a worker pool, keep the UI thread responsive.
- Pipelines — fetch → parse → validate → persist, each step a non-blocking stage.
- Timeouts and fallbacks — race a slow call against a timer, take whichever settles first.
- Return type of an Active Object — method calls return Futures instead of blocking.
Code Examples¶
Java — Future (the classic, blocking view)¶
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(() -> {
Thread.sleep(200); // pretend this is slow IO
return 6 * 7;
});
// ... do other work here ...
Integer answer = future.get(); // blocks until the task finishes
System.out.println(answer); // 42
pool.shutdown();
Future (the 2004-era interface) only lets you get(), cancel(), isDone(), isCancelled(). It cannot compose — that limitation is exactly why CompletableFuture exists.
Java — CompletableFuture (composable, non-blocking)¶
import java.util.concurrent.*;
CompletableFuture<String> greeting =
CompletableFuture
.supplyAsync(() -> fetchUser(42)) // runs on a pool thread
.thenApply(User::name) // transform: User -> String
.thenApply(name -> "Hello, " + name); // transform again
// Non-blocking consumption:
greeting.thenAccept(System.out::println);
// Or block at the very end if you must:
System.out.println(greeting.join());
Java — the Promise (write) side, made explicit¶
// An unsettled CompletableFuture IS a promise.
CompletableFuture<String> promise = new CompletableFuture<>();
// Consumer wires up the read side now:
promise.thenAccept(v -> System.out.println("Got: " + v));
// A producer thread completes it later:
new Thread(() -> {
String result = doSlowWork();
promise.complete(result); // fulfill -> callback fires
// promise.completeExceptionally(new IOException()); // reject alternative
}).start();
JavaScript — Promise (resolve / reject)¶
// The executor receives the write side: resolve() and reject().
const pricePromise = new Promise((resolve, reject) => {
fetchPrice("AAPL", (err, price) => {
if (err) reject(err); // -> rejected
else resolve(price); // -> fulfilled
});
});
// The read side: .then / .catch
pricePromise
.then(price => price * 1.1)
.then(withTax => console.log(withTax))
.catch(err => console.error("failed:", err));
// async/await is the same Promise, read more linearly:
async function show() {
try {
const price = await pricePromise; // "block" without blocking the thread
console.log(price);
} catch (err) {
console.error(err);
}
}
Scala — Future + Promise split crisply¶
import scala.concurrent.{Future, Promise}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Success, Failure}
val p = Promise[Int]() // the WRITE side
val f: Future[Int] = p.future // the READ side, derived from the promise
f.onComplete { // consumer attaches to the read side
case Success(v) => println(s"value = $v")
case Failure(e) => println(s"error = ${e.getMessage}")
}
p.success(42) // producer fulfills (or p.failure(ex) to reject)
Scala makes the lesson unmissable: Promise and Future are two different types, and you obtain the read side via promise.future.
Coding Patterns¶
- Fire-and-transform:
supplyAsync(...).thenApply(...)— start work, map the result. - Fire-and-forget side effect:
thenAccept(...)/thenRun(...)— consume, no new value. - Sequential dependency (flatMap):
thenCompose(x -> anotherFuture(x))— when step 2 itself returns a Future. - Parallel join:
a.thenCombine(b, (x, y) -> merge(x, y))— two independent Futures into one. - Fan-out / fan-in:
CompletableFuture.allOf(f1, f2, f3)then collect results. - Recover:
.exceptionally(ex -> fallbackValue)— supply a default on failure.
Clean Code¶
✓ Name Futures by the value they will hold, not by "future": userProfile, not userFuture everywhere — though a Future suffix is acceptable when it disambiguates a blocking value from its placeholder.
✓ Prefer composition to nested get() calls. Chained stages read top-to-bottom; nested blocking reads inside-out.
✗ Don't get() in the middle of a chain just to feed the next stage — use thenCompose.
✗ Don't leave a Future un-consumed. A CompletableFuture whose exceptional completion nobody observes is a silent bug.
✓ Pass an explicit Executor to *Async methods in production code rather than relying on the hidden default pool.
Best Practices¶
- Avoid blocking
get()/join()inside async code. Compose instead; reserve blocking for the program's top edge (e.g.main). - Always provide an explicit executor to
supplyAsync/thenApplyAsyncin server code — don't silently use the sharedForkJoinPool.commonPool(). - Always attach error handling (
exceptionally/handle/whenComplete) to the end of every chain. - Keep callbacks short and non-blocking — a blocking callback ties up a pool thread.
- Complete a Promise exactly once; treat the second
complete()as a no-op, never as logic. - Set timeouts (
orTimeout,completeOnTimeout) so a stuck dependency can't hang forever.
Edge Cases & Pitfalls¶
- The swallowed exception.
future.thenApply(f);— if the Future rejects and you never callexceptionally/get/whenComplete, the error vanishes. No log, no crash. - Blocking inside a pool thread. Calling
get()on aForkJoinPoolworker can starve the pool — every worker waits on results that need a free worker to produce. thenApplyruns where the previous stage completed. It may run on the completing thread or the calling thread — surprising if you assumed "the pool." UsethenApplyAsyncto force an executor.- Cancellation is weak.
CompletableFuture.cancel(true)does not interrupt the runningsupplyAsynctask; it just marks the Future cancelled. The work keeps running. - Already-completed Futures run callbacks synchronously. Attaching
thenApplyto an already settled Future runs the function immediately on the current thread.
Common Mistakes¶
- Blocking right after starting:
supplyAsync(...).get()on the same line — that's just a slow synchronous call with extra ceremony. - Sequential awaits instead of parallel:
a.get(); b.get();runs A then B; useallOf(a, b)to overlap them. - Catching the wrong exception type:
get()wraps the cause in anExecutionException; you must unwrape.getCause(). - Mutating shared state in callbacks without synchronization, assuming "it's all one thread." It isn't.
- Forgetting the chain is lazy on errors — adding logging after an
exceptionallythat already recovered won't see the original error.
Tricky Points¶
join()vsget(): both block;get()throws checkedExecutionException/InterruptedException,join()throws uncheckedCompletionException.join()is friendlier inside lambdas.thenApplyvsthenCompose:thenApply(f)wherefreturns aFuturegives you a nestedFuture<Future<T>>. UsethenComposeto flatten — it's theflatMapof Futures.supplyAsyncvsrunAsync:supplyAsyncreturns a value (Supplier);runAsyncreturnsVoid(Runnable).- Eager vs lazy: A Java/JS Future already started the moment you created it. A Rust
Futuredoes nothing until polled. This changes how cancellation and resource use behave.
Test Yourself¶
- What two responsibilities does the Future/Promise pattern separate?
- Which methods form the write side of a
CompletableFuture? - Why is
supplyAsync(...).get()usually a code smell? - What happens to downstream
thenApplystages when an upstream stage throws? - What is the difference between
thenApplyandthenCompose? - Does
CompletableFuture.cancel(true)stop a runningsupplyAsynctask?
Answers: (1) starting async work vs consuming the result; (2)
complete/completeExceptionally; (3) it blocks immediately, discarding the async benefit; (4) they're skipped, the error jumps toexceptionally/handle; (5)thenApplymaps to a plain value,thenComposeflattens a returned Future; (6) no — it marks the Future cancelled but the task keeps running.
Tricky Questions¶
- If a Future is already completed when you attach
thenApplyAsync, does the callback still run on the executor? Yes —*Asyncalways dispatches to the executor, even for already-settled Futures, whereas non-async may run inline. - Can two threads both
complete()the sameCompletableFuture? They can both call it, but only the first wins; the second returnsfalse. It is race-safe. - Does
exceptionallycatch an exception thrown inside itself? No — you'd need another stage after it. Errors in a recovery handler propagate further down.
Cheat Sheet¶
CREATE
CompletableFuture.supplyAsync(supplier[, executor]) // value
CompletableFuture.runAsync(runnable[, executor]) // no value
new CompletableFuture<>() // empty promise
TRANSFORM (read side)
thenApply(fn) T -> U (map)
thenCompose(fn) T -> Future<U>(flatMap)
thenCombine(other,fn)(T,U) -> V (join two)
thenAccept(consumer) side effect, no result
thenRun(runnable) ignore value, run action
COMBINE MANY
allOf(f1..fn) completes when ALL done
anyOf(f1..fn) completes when FIRST done
ERRORS
exceptionally(fn) ex -> fallback value
handle((v,ex) -> ...) both branches
whenComplete((v,ex)) peek, don't transform
WRITE side
complete(value)
completeExceptionally(ex)
BLOCK (last resort)
get() throws checked ExecutionException
join() throws unchecked CompletionException
ASYNC variants: append "Async" to run the stage on an executor.
Summary¶
A Future is a read-only proxy for a not-yet-computed value; a Promise is the write-once producer side that completes it with a value or an error. The pattern separates starting async work from consuming its result. Prefer composition (thenApply, thenCompose, thenCombine, allOf) over blocking get(), attach error handling to every chain, and be deliberate about which executor runs each callback. Java's Future is the blocking ancestor; CompletableFuture is the composable modern tool. The same idea reappears as JS Promise, Scala Future/Promise, C++ std::future, and Rust Future.
What You Can Build¶
- A parallel web-page aggregator that fetches 5 URLs concurrently and combines them with
allOf. - A non-blocking price service that races a live quote against a cached fallback via
anyOf. - A retry wrapper that re-attaches a new attempt in
exceptionallyon failure. - A small async pipeline (
fetch → parse → validate → store) with no blocking and a single error handler.
Further Reading¶
- Doug Lea, Concurrent Programming in Java, ch. on result-bearing tasks.
- Baker & Hewitt (1977) — the original "future" concept in Actor systems.
java.util.concurrent.CompletableFutureJavadoc — read the stage-method table top to bottom.- MDN: Using Promises — the cleanest plain-language introduction to the read/write split.
Related Topics¶
- Active Object — its asynchronous method calls return Futures.
- Thread Pool — supplies the threads a Future's work runs on.
- Proactor — completion-based async IO, a natural producer of Futures.
- Producer–Consumer — a Promise/Future is a one-shot, single-slot producer/consumer.
Diagrams & Visual Aids¶
The read/write split:
Three states:
A simple composition pipeline:
In this topic
- junior
- middle
- senior
- professional