Skip to content
  • CompletableFuture · Java

CompletableFuture

LearnViaNotes @learnvianotes· 4 min read

CompletableFuture composes asynchronous steps into a pipeline — transform, chain, combine and recover — without blocking a thread on each get().

Transform and combine

Java
import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) {
        CompletableFuture<Integer> price = CompletableFuture.supplyAsync(() -> 200);
        CompletableFuture<Double> discount = CompletableFuture.supplyAsync(() -> 0.9);

        double total = price
            .thenCombine(discount, (p, d) -> p * d)   // wait for BOTH, merge
            .thenApply(x -> x + 15)                   // transform the value (shipping)
            .join();                                  // block for the final result
        System.out.println("total = " + total);

        CompletableFuture.supplyAsync(() -> "order-42")
            .thenApply(String::toUpperCase)
            .thenAccept(id -> System.out.println("placed " + id))   // consume, returns CF<Void>
            .thenRun(() -> System.out.println("email queued"))      // no input, no output
            .join();
    }
}
Output
total = 195.0
placed ORDER-42
email queued

thenApply vs thenCompose

thenApply is map: the function returns a plain value. thenCompose is flatMap: the function itself returns a CompletableFuture, and you want the inner result — not a nested CompletableFuture<CompletableFuture<T>>.

Java
import java.util.List;
import java.util.concurrent.CompletableFuture;

public class Main {
    static CompletableFuture<String> findUser(int id) {
        return CompletableFuture.supplyAsync(() -> "user" + id);
    }

    static CompletableFuture<List<String>> findOrders(String user) {
        return CompletableFuture.supplyAsync(() -> List.of(user + ":book", user + ":pen"));
    }

    public static void main(String[] args) {
        List<String> orders = findUser(7)
            .thenCompose(Main::findOrders)        // async step depending on the previous one
            .join();
        System.out.println(orders);
    }
}
Output
[user7:book, user7:pen]
MethodFunction shapeAnalogy
thenApply(f)T -> Umap
thenCompose(f)T -> CompletionStage<U>flatMap — sequential async calls
thenCombine(other, f)(T, U) -> Vzip two independent futures
thenAccept(c) / thenRun(r)T -> void / () -> voidside effect at the end
allOf(cfs...)→ CompletableFuture<Void>wait for all
anyOf(cfs...)→ CompletableFuture<Object>first to finish wins
...Async variantssame, but callback runs on the pooldon't hog the completing thread

Waiting for many: allOf

Java
import java.util.List;
import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) {
        List<CompletableFuture<Integer>> calls = List.of("alpha", "beta", "gamma").stream()
            .map(s -> CompletableFuture.supplyAsync(() -> s.length()))
            .toList();

        CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();
        List<Integer> lengths = calls.stream().map(CompletableFuture::join).toList();  // all done: no waiting
        System.out.println(lengths + " sum=" + lengths.stream().mapToInt(Integer::intValue).sum());
    }
}
Output
[5, 4, 5] sum=14

Handling errors

Java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CompletableFuture<Integer> failing = CompletableFuture.supplyAsync(() -> Integer.parseInt("x"));

        int recovered = failing.exceptionally(ex -> -1).join();          // fallback value
        System.out.println("exceptionally -> " + recovered);

        String handled = failing.handle((value, ex) ->                   // sees success OR failure
            ex == null ? "ok " + value : "failed: " + ex.getCause().getClass().getSimpleName()).join();
        System.out.println("handle -> " + handled);

        try {
            failing.join();                                              // unchecked wrapper
        } catch (CompletionException e) {
            System.out.println("join threw CompletionException, cause " + e.getCause().getClass().getSimpleName());
        }
        try {
            failing.get();                                               // checked wrapper
        } catch (ExecutionException e) {
            System.out.println("get threw ExecutionException, cause " + e.getCause().getClass().getSimpleName());
        }
    }
}
Output
exceptionally -> -1
handle -> failed: NumberFormatException
join threw CompletionException, cause NumberFormatException
get threw ExecutionException, cause NumberFormatException
MethodCalled onCan change result?
exceptionally(fn)failure onlyYes — supply a fallback
handle((v, ex) -> ...)success and failureYes — map either to a new value
whenComplete((v, ex) -> ...)success and failureNo — side effects (logging); original result passes through
orTimeout(t, unit) / completeOnTimeout(v, t, unit)Java 9+Fail / default after a timeout

Use your own executor

Java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        try (ExecutorService io = Executors.newFixedThreadPool(2, r -> new Thread(r, "io-worker"))) {
            String where = CompletableFuture
                .supplyAsync(() -> Thread.currentThread().getName(), io)   // runs on our pool
                .join();
            System.out.println("ran on " + where);
        }
    }
}
Output
ran on io-worker

join() vs get()?

thenApply vs thenCompose?

Future vs CompletableFuture?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report