Future vs CompletableFuture in Java
Fast recall
Quick revision
Core answer
Whether you understand the limitations of Future that motivated CompletableFuture, and can use the fluent chaining API
Blocking on CompletableFuture with get() everywhere, defeating its purpose. Or not handling exceptions in the chain, causing silent failures
Show a real async pipeline using thenCompose for dependent tasks, allOf for parallel fan-out, and exceptionally for error recovery
Interview-ready guide
Interview answer
Future represents the result of an asynchronous computation — you submit a task to an executor, receive a Future back, and retrieve the result when needed. CompletableFuture extends this model significantly: it's non-blocking, composable, and designed for building full async pipelines rather than just waiting for individual tasks.
Future limitations:
get()blocks the calling thread — no callbacks, no chaining, just block or poll- No way to compose or combine futures without writing blocking glue code
CompletableFuture capabilities:
thenApply(): transform the result (one-to-one, like map)thenCompose(): chain dependent async operations — function returns aCompletableFuture; avoids nested futuresthenCombine(): merge two independent futures running in parallelexceptionally(): recover with a default value on exceptionhandle()/whenComplete(): receive both result and exception
cancel() gotcha:
- Does not interrupt the underlying computation — marks the future as cancelled, but the task keeps running
I recommend CompletableFuture for all async work in modern Java. Future.get() blocking defeats the purpose of asynchronous execution.
Future vs CompletableFuture in Java
Direct Answer
Future (Java 5) represents a pending result but only supports blocking get() — you can't chain callbacks, compose results, or handle errors declaratively. CompletableFuture (Java 8) adds a fluent API for non-blocking composition: thenApply, thenCompose, exceptionally, allOf/anyOf. It can also be completed manually, making it far more powerful for building async pipelines.
Deep Dive & Architecture
CompletableFuture provides three recovery/observation hooks. exceptionally(Function<Throwable, T>) — runs only on failure; returns a replacement value of type T; the cleanest "fallback on error" API. handle(BiFunction<T, Throwable, U>) — runs on both success and failure; sees both value and throwable (one is null); can transform type T → U and swallow the exception. whenComplete(BiConsumer<T, Throwable>) — runs on both but is observe-only: can't change the result, exception propagates downstream unchanged.
Key rule: every CompletableFuture chain should terminate in exceptionally, handle, or have someone call .get()/.join() on it.
Future (Java 5) represents a pending result but only supports blocking get() — you can't chain callbacks, compose results, or handle errors declaratively. CompletableFuture (Java 8) adds a fluent API for non-blocking composition: thenApply, thenCompose, exceptionally, allOf/anyOf. It can also be completed manually, making it far more powerful for building async pipelines.
A common mistake is calling .get() on a CompletableFuture and missing the whole point. Not knowing that *Async variants run on commonPool and shouldn't be used for blocking work without a custom executor
Common Mistakes & Pitfalls
Blocking on CompletableFuture with get() everywhere, defeating its purpose. Or not handling exceptions in the chain, causing silent failures
Check your understanding
Answer these without looking back at the article.
- What is `CompletableFuture` in Java? Why was it added when `Future` already existed?
- How do you chain asynchronous operations using CompletableFuture? Explain the difference between thenApply, thenCompose, and exceptionally with examples.
- What's the difference between `thenCompose` and `thenCombine` in `CompletableFuture`?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.