Plain Future only lets you block for a result — you cannot chain a callback, combine two futures, recover from failure declaratively, or complete it from outside. CompletableFuture fixes all of these by implementing CompletionStage.
The four limitations of Future
1. You can only block. The single way to read the value is get(), which parks the calling thread until the task finishes. There is no "when it's ready, do X" — you either block or poll isDone() in a loop, both of which waste a thread.
2. No composition. You cannot say "take the result of future A and feed it into computation B" without blocking on A first. Pipelining two async calls means a thread sits idle waiting for the first.
3. No combining. There is no built-in way to wait for several futures and merge their results, or to race them and take the first.
4. No exception handling and no manual completion. A failed Future only throws at get(); you cannot attach a recovery step. And you cannot create a Future and complete it yourself later (useful for adapting callback-based APIs).
How CompletableFuture answers each
CompletableFuture
.supplyAsync(() -> fetchUserId()) // async, non-blocking start
.thenApply(id -> loadProfile(id)) // composition: chain a transform
.thenCombine( // combine with another future
CompletableFuture.supplyAsync(this::loadSettings),
(profile, settings) -> merge(profile, settings))
.exceptionally(ex -> Profile.guest()) // declarative recovery
.thenAccept(this::render); // react when ready, no get()
// manual completion: bridge a legacy callback API
CompletableFuture<String> cf = new CompletableFuture<>();
legacyApi.onResult(cf::complete); // complete it from outside
legacyApi.onError(cf::completeExceptionally);
No thread blocks anywhere in that pipeline; each stage fires when its predecessor completes.