Comprehensive Guide to Java CompletableFuture: Asynchronous Tasks, Callbacks, and Multi‑Future Composition

This article explains Java's CompletableFuture API introduced in JDK 8, covering how to create asynchronous tasks with supplyAsync and runAsync, use various callback methods such as thenApply, thenAccept, thenRun, whenComplete, handle, and combine multiple futures with thenCombine, allOf, anyOf, providing code examples and execution results.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Comprehensive Guide to Java CompletableFuture: Asynchronous Tasks, Callbacks, and Multi‑Future Composition
CompletableFuture

is a new feature in JDK 8 that implements the CompletionStage interface and extends Future, adding asynchronous callbacks, stream processing, and the ability to compose multiple futures for smoother concurrent programming in Java.

1. Creating Asynchronous Tasks

1.1 supplyAsync

supplyAsync

creates an asynchronous task that returns a value. It provides two overloads: one that uses the default thread pool ( ForkJoinPool.commonPool()) and another that accepts a custom Executor.

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

Test code:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
        System.out.println("do something....");
        return "result";
    });
    System.out.println("Result -> " + cf.get());
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
        System.out.println("do something....");
        return "result";
    }, executorService);
    System.out.println("Result -> " + cf.get());
}

Test results are shown in the accompanying screenshots.

1.2 runAsync

runAsync

creates an asynchronous task without a return value. It also has two overloads: one using the default thread pool and another that accepts a custom executor.

public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)

Test code:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
        System.out.println("do something....");
    });
    System.out.println("Result -> " + cf.get());
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
        System.out.println("do something....");
    }, executorService);
    System.out.println("Result -> " + cf.get());
}

Test results are displayed in the article.

2. Asynchronous Callback Handling

2.1 thenApply and thenApplyAsync

thenApply

executes a function after the preceding task completes, receiving the previous result as input and returning a new value. thenApplyAsync performs the same operation but runs in a separate thread (default ForkJoinPool.commonPool()) and can accept a custom executor.

CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> cf2 = cf1.thenApplyAsync(result -> result + 2);
System.out.println("cf1 result -> " + cf1.get());
System.out.println("cf2 result -> " + cf2.get());

The screenshots illustrate that thenApply runs in the same thread as its predecessor, while thenApplyAsync may use a different thread.

2.2 thenAccept and thenAcceptAsync

thenAccept

runs a consumer after the previous task finishes; it receives the result but returns no value. The async variant behaves similarly but can run on a separate thread.

CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Void> cf2 = cf1.thenAccept(result -> System.out.println("Result: " + result));
System.out.println("cf1 result -> " + cf1.get());
System.out.println("cf2 result -> " + cf2.get());

2.3 thenRun and thenRunAsync

thenRun

executes a runnable after the previous future completes, without receiving any input or producing a result. The async version may run on a different thread.

CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Void> cf2 = cf1.thenRun(() -> System.out.println("Task finished"));
System.out.println("cf1 result -> " + cf1.get());
System.out.println("cf2 result -> " + cf2.get());

2.4 whenComplete and whenCompleteAsync

whenComplete

runs a callback after a future finishes, receiving both the result (or null) and any exception that occurred. The async variant may execute in another thread.

CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
    int a = 1 / 0; // will throw ArithmeticException
    return 1;
});
CompletableFuture<Integer> cf2 = cf1.whenComplete((result, e) -> {
    System.out.println("Previous result: " + result);
    System.out.println("Previous exception: " + e);
});
System.out.println("cf2 result -> " + cf2.get());

2.5 handle and handleAsync

handle

is similar to whenComplete but allows the callback to return a new value, effectively transforming the result or providing a fallback when an exception occurs.

CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> cf2 = cf1.handle((result, e) -> {
    System.out.println("Previous result: " + result);
    System.out.println("Previous exception: " + e);
    return result + 2;
});
System.out.println("cf2 result -> " + cf2.get());

3. Multi‑Task Composition

3.1 thenCombine, thenAcceptBoth, runAfterBoth

These methods combine two CompletableFuture instances. All three wait for both tasks to complete successfully before proceeding. thenCombine receives both results and returns a new value; thenAcceptBoth receives both results but returns nothing; runAfterBoth runs after both complete without any input.

CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> cf3 = cf1.thenCombine(cf2, (a, b) -> a + b);
System.out.println("cf3 result -> " + cf3.get());

3.2 applyToEither, acceptEither, runAfterEither

These methods also combine two futures, but they trigger as soon as **any** one of the futures completes. applyToEither returns a value, acceptEither consumes the result without returning, and runAfterEither runs a runnable with no input.

3.3 allOf and anyOf

CompletableFuture.allOf

returns a new future that completes only when **all** supplied futures finish; if any completes exceptionally, the resulting future throws an exception. anyOf completes when **any** of the supplied futures finishes, returning the result of the first completed task.

CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
    Thread.sleep(2000);
    return "cf1 finished";
});
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
    Thread.sleep(5000);
    return "cf2 finished";
});
CompletableFuture<Void> all = CompletableFuture.allOf(cf1, cf2);
System.out.println("All result -> " + all.get());

CompletableFuture<Object> any = CompletableFuture.anyOf(cf1, cf2);
System.out.println("Any result -> " + any.get());

The article concludes with a brief thank‑you note and source reference.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencyThreadPoolCompletableFutureAsyncFutureComposition
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.