What is the difference between get() and join() on a CompletableFuture, and when would you choose each?
answer
- get = checked (Interrupted/Execution/Timeout)
- join = unchecked (CompletionException)
- same cause inside, different wrapper class
- join fits lambdas/streams; get for timeout/interrupt
- never block a ForkJoinPool worker thread
basics
~10 sBoth block until the result is ready. get() throws checked exceptions (InterruptedException, ExecutionException), so callers must handle them. join() throws unchecked exceptions (CompletionException), so it's cleaner inside lambdas and stream pipelines.
solid answer
~50 s`get()` and `join()` both block the calling thread until the future settles, then return the value or throw if it failed. The difference is the exception contract. `get()` is the legacy `Future` method: it throws **checked** `InterruptedException` and `ExecutionException` (wrapping the failure cause), forcing try/catch or a throws clause. `get(timeout, unit)` adds a `TimeoutException`. `join()` is CompletableFuture-specific and throws only **unchecked** exceptions — a `CompletionException` wrapping the failure, or `CancellationException` — so it fits cleanly inside lambdas, streams, and method references where checked exceptions are awkward. Practically: use `join()` in functional/composition code, use `get()` (especially the timeout overload) when you need a bounded wait or to respond to interruption explicitly. Note `get()` does not wrap a CancellationException; `join()` preserves it as-is too. Neither should be called on the common ForkJoinPool worker thread, as blocking there starves the pool.
go deeper
Knows both block for the result and that get throws checked exceptions while join throws unchecked ones.
Picks join for stream/lambda code and get(timeout) for bounded waits, and knows the exception wrapper classes differ (ExecutionException vs CompletionException) but share the same cause.
Explains the Future-interface inheritance reason for get, the TimeoutException-doesn't-cancel subtlety, cancellation behavior, and warns about blocking ForkJoinPool workers.
Sets team conventions: prefer non-blocking composition over either, reserve bounded get() for true sync boundaries, and avoids pool starvation by isolating blocking onto dedicated executors.
### The problem both methods solve A `CompletableFuture<T>` is asynchronous — its result may not exist yet. Sometimes you reach a point where you genuinely need the value **now**, synchronously, before continuing. Both `get()` and `join()` **block** the current thread until the future is settled, then either return the value or throw to signal a failure. ### get() — the inherited Future contract `CompletableFuture` implements `java.util.concurrent.Future`, so it must provide: ```java T get() throws InterruptedException, ExecutionException; T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; ``` These are **checked** exceptions — the compiler forces you to catch or declare them: - `InterruptedException` — the blocked thread was interrupted while waiting. - `ExecutionException` — the future failed; the real failure is `e.getCause()`. - `TimeoutException` (timeout overload only) — the deadline passed before the future settled. The future itself is **not** cancelled or completed by this timeout; you just stop waiting. ### join() — the CompletableFuture-native blocker ```java T join(); // no checked exceptions ``` `join()` blocks the same way but throws only **unchecked** exceptions: - `CompletionException` — the future failed; the real failure is `getCause()`. - `CancellationException` — the future was cancelled. Because it declares no checked exceptions, `join()` works seamlessly where checked exceptions are forbidden or painful — inside `Stream.map(CompletableFuture::join)`, lambdas, and method references. ### The exception-wrapping difference, precisely Suppose the future failed with `new IllegalStateException("x")`: - `get()` throws `ExecutionException` → `getCause()` is the `IllegalStateException`. - `join()` throws `CompletionException` → `getCause()` is the `IllegalStateException`. Different wrapper class, same cause inside. For cancellation: `get()` throws `CancellationException` (an unchecked exception, even though get declares checked ones), and `join()` also throws `CancellationException`. ### Choosing between them - **Use `join()`** in composition/functional code where you want clean call sites and no checked-exception noise. - **Use `get(timeout, unit)`** when you need a **bounded wait** — you must not block forever — or when you need to react to thread interruption explicitly. - **Use plain `get()`** mainly for compatibility with code written against the `Future` interface. ### Critical caveat: don't block pool threads If a future is running on the **common ForkJoinPool** (the default executor for `supplyAsync`/`runAsync` without an explicit executor) and you call `get`/`join` from one of that pool's own worker threads, you can **starve** the pool — the worker is parked waiting for work that needs a worker. The fix is to compose asynchronously (`thenApply`, `thenCompose`) instead of blocking, or block only on a thread that is not a pool worker. ### Tiny example ```java CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> 21 * 2); int a = cf.join(); // 42, no try/catch needed try { int b = cf.get(1, TimeUnit.SECONDS); // bounded, but checked exceptions } catch (InterruptedException | ExecutionException | TimeoutException e) { // handle } ```
- Why does join() exist when Future already has get()?To avoid checked-exception noise in functional/composition code. join's unchecked CompletionException lets it be used directly as a method reference (e.g. .map(CompletableFuture::join)) inside streams and lambdas.
- Does get(timeout) cancel the future when the timeout fires?No. It only stops the caller from waiting and throws TimeoutException; the future keeps running and may still complete later. Use orTimeout/cancel for actually terminating it.
saying these in an interview costs you the question
- Saying join() never throws — it throws CompletionException/CancellationException
- Claiming get() returns the raw exception — it wraps it in ExecutionException
- Thinking get(timeout) cancels or completes the future on timeout — it doesn't
- Using join() inside a ForkJoinPool task and risking pool starvation