CompletableFuture
CompletableFuture represents an async computation. It can be manually completed (complete(), completeExceptionally()) or created from async operations (supplyAsync(), runAsync()). It supports chaining via thenApply(), thenCompose(), thenCombine().
CompletableFuture = a promise note chain. thenApply = 'when pizza arrives, add cheese.' thenCompose = 'when pizza arrives, order dessert too.' Errors vanish into a black hole unless you attach an exceptionally() safety net.
thenApply() transforms the result (like map). thenCompose() chains another CompletableFuture (like flatMap — avoids nested CompletableFuture<CompletableFuture<T>>). thenCombine() merges two independent futures. exceptionally() handles errors. allOf() / anyOf() combine multiple futures. Without an explicit executor, callbacks run on the ForkJoinPool.commonPool() — avoid blocking operations in callbacks. thenApplyAsync() dispatches the callback to an executor thread, preventing callback chains from blocking the upstream thread.
CompletableFuture's error model: each stage can fail independently; exceptionally() recovers; handle() handles both success and failure. join() is like get() but throws CompletionException (unchecked) instead of ExecutionException. Cancellation propagates only forward (downstream) not backward (upstream) — cancelling a derived future does not cancel the original computation. For structured async code in Java 21+, virtual threads combined with blocking code are often simpler and more readable than deeply nested CompletableFuture chains. Project Loom's structured concurrency (StructuredTaskScope) provides deterministic cancellation semantics.
CompletableFuture enables non-blocking async pipelines: thenApply() transforms results, thenCompose() chains async steps (avoiding nested futures), exceptionally() or handle() manages errors. The critical operational gotcha: without specifying an executor, callbacks run on the ForkJoinPool common pool — blocking inside a callback starves the pool for other tasks. Always pass an explicit executor to *Async methods in production code and handle exceptions explicitly, as unhandled CompletableFuture exceptions are silently swallowed.
Unhandled exceptions in CompletableFuture stages are silently swallowed unless you call get()/join() or attach an exceptionally()/whenComplete() handler — the exception disappears and the future stays permanently incomplete.