A placeholder for a value that doesn't exist yet
A future is a handle to a result that will exist at some point, produced by work running elsewhere (another thread, another process, a network call). At the moment you receive the future, the value usually isn't ready — but the future object itself exists immediately, and you can ask it "are you done," block until it is, or (in richer APIs) attach a continuation that runs automatically once it is.
A promise is the write side of that same relationship: the object that eventually fulfills the future with a value, or fails it with an error. In languages that keep the two roles separate, a promise and its future are two different objects handed to two different parties — the producer holds the promise, the consumer holds the future. JavaScript's Promise is a slight naming curiosity here: the single object it gives you plays both roles simultaneously — you get resolve/reject closures (the promise/write side) inside the constructor's executor function, and the object itself is what callers consume (the future/read side). Java's original Future (from Java 5) was read-only and not composable at all; CompletableFuture (Java 8) fused the two roles into one class, adding complete()/completeExceptionally() as the write side. Kotlin's Deferred is the read side, produced by the async coroutine builder, with CompletableDeferred available when you need to construct and complete one manually — the cleanest example of a real separate promise/future pair in the languages this roadmap covers.
Two ways to consume a future
Blocking-get. Call .get()/.join()/.result() and the calling thread parks until the value (or exception) is available. Simple to reason about, but it ties up a thread for the entire wait — which defeats a large part of the point of doing the work asynchronously in the first place. Note that this option doesn't even exist for JavaScript's Promise: a single-threaded event loop that blocked on a pending promise would deadlock itself, so Promise deliberately only offers the next option.
Callback / then-style composition. Register a continuation — .thenApply(), .thenAccept(), .then() — that the runtime invokes automatically once the future settles, without parking the calling thread. The continuation runs later, on whatever executor the future implementation chooses (a thread pool for CompletableFuture, the microtask queue for JS Promise). This is what makes futures composable: every .then()-style call returns a new future, so you can chain, branch, and combine without ever blocking.
async/await is worth naming here even though it's not this subtopic's job to teach it (that's topic 10, Async & Event-Driven Concurrency): it is purely syntactic sugar over the callback-style model above, letting you write code that looks sequential while the compiler/runtime rewrites it into the same chain of continuations under the hood. Everything in this subtopic — the future/promise object, its states, and how you compose several of them — is the substrate that async/await sits on top of.
Composing futures: the part interviewers actually probe
Sequencing (chaining). Transforming a future's eventual value with a synchronous function (thenApply in Java, a .then() handler that returns a plain value in JS) is straightforward. It gets interesting when the transformation itself is asynchronous — i.e., it returns another future. Applying a synchronous-style transform there naively gives you a future-of-a-future, which is almost never what you want. Java solves this with a dedicated method, thenCompose, that flattens the nesting; JavaScript's .then() handles both cases with the same method by auto-flattening any thenable you return from inside it. Knowing the name thenCompose and being able to say "it's flatMap, thenApply is map" is a very standard interview beat.
Combining independent futures. Two futures that don't depend on each other (fetch a user, fetch their orders) can run concurrently and be joined once both finish: thenCombine in Java, a manual awaitAll over multiple Deferred in Kotlin, Promise.all in JS. Worth memorizing as a concrete API-shape difference: Java's CompletableFuture.allOf(...) returns CompletableFuture<Void> — you still have to reach back into each original future individually to get its value — while JavaScript's Promise.all([...]) directly resolves to an array of the results, in order. Both are fail-fast: the first rejection short-circuits the combined result. Promise.allSettled (and the equivalent of gathering results manually in Java/Python) is the "wait for everyone regardless of failures" alternative.
Racing. Promise.race() / CompletableFuture.anyOf() resolve with whichever input future settles first — the standard building block for implementing a timeout: race your real computation against a future that rejects after a fixed delay.
Exception propagation through a chain. An exception raised at any stage of a chain skips every subsequent transformation stage (thenApply, thenCompose, .then()'s success handler) until it reaches a stage specifically designed to intercept failures — exceptionally/handle in Java, .catch() (or the second argument to .then()) in JS. This mirrors synchronous try/catch propagating up a call stack, except it's happening across asynchronous continuations instead of stack frames. Java wraps the original cause in a CompletionException (or ExecutionException if you're blocking with get()), so "unwrap the cause" is a real detail worth knowing, not trivia.
Cancellation. This is where the languages diverge the most, and it's a favorite "how deep do you actually understand this" question:
- Java's original
Future.cancel(mayInterruptIfRunning) is best-effort — it can set a thread interrupt flag, but it cannot force a computation that ignores interruption to actually stop.
CompletableFuture.cancel() doesn't even do that much: it just completes the future exceptionally with a CancellationException. It does not interrupt whatever thread is actually running the underlying computation.
- Kotlin's structured concurrency has the most coherent story here: cancelling a parent
Job cooperatively propagates CancellationException into every child coroutine at their next suspension point, and well-behaved coroutine code is expected to check for cancellation or let it propagate rather than swallowing it.
- Plain JavaScript
Promises have no built-in cancellation at all — once created, a promise runs to completion. Real cancellation requires an external mechanism (an AbortController/AbortSignal threaded through the operation) that the async work has to explicitly check.
Interview gotchas
- A future doesn't parallelize anything by itself. Wrapping a computation in a future only decouples when it's produced from when it's consumed — the computation still has to actually run on some thread/coroutine/executor. Forgetting this leads to "why didn't this get faster" confusion.
thenApply vs. thenCompose nesting bugs — passing a function that returns a future to thenApply produces CompletableFuture<CompletableFuture<T>>, a classic and very common mistake.
- Silently dropped exceptions. If you never attach an
exceptionally/.catch() handler, the failure doesn't disappear — it surfaces (wrapped) the moment something finally calls get()/join(). In JavaScript specifically, an unhandled promise rejection doesn't get "caught" anywhere; it triggers a runtime warning (and can crash a Node process) rather than propagating anywhere useful.
- Blocking inside a continuation that runs on a shared pool.
CompletableFuture's default continuations run on the common ForkJoinPool unless you specify an executor — doing blocking I/O inside one can starve unrelated asynchronous work across the entire JVM.
- Choosing the wrong combination semantics. Defaulting to fail-fast (
allOf/Promise.all) when you actually need every result regardless of individual failures (Promise.allSettled, or manually catching per-future in Java/Python) silently discards partial progress.
Where this leads
This subtopic covered the future/promise object and how to compose several of them explicitly. Two things build directly on it: topic 9 (Thread Pools & Executors) is what actually produces most futures in practice — submitting a task to an executor is what hands you back the future you've been composing here — and topic 10 (Async & Event-Driven Concurrency) shows how async/await and the event-loop model let you express the same compositions with sequential-looking syntax instead of explicit chaining.