handle's callback always runs, receiving either the computed value with a null exception, or a null value with the exception that occurred, so both paths are handled in one place. Compare this to a plain get(), which would need a try/catch instead.
CompletableFuture<Integer> ok = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<String> okHandled = ok.handle((value, ex) -> ex == null ? "ok:" + value : "error");
CompletableFuture<Integer> failing = CompletableFuture.supplyAsync(() -> {
throw new IllegalStateException("boom");
});
CompletableFuture<String> failHandled =
failing.handle((value, ex) -> ex == null ? "ok:" + value : "error:" + ex.getCause().getMessage());
System.out.println(okHandled.get());
System.out.println(failHandled.get());
ok:10
error:boom
Run this yourself in the Online Java Compiler, spin up a live REST API in the API Sandbox, or practise with Java interview questions.
Published 2026-09-27