Java 12 - Exception Handling in CompletionStage (CompletableFuture)

Java 12 added five new methods to CompletionStage interface. These methods related to error recovery and are add-ons to the existing method exceptionally(Function<Throwable,T>), which recovers from a failed computation by turning the exception into a normal result. As class that implementing CompletionStage, we can have this enhancements through CompletableFuture.

CompletionStage::exceptionally

Here a sample code using existing exceptionally method in CompletableFuture:

CompletableFutureExample.java
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {

    public static void main(String[] args) throws Exception {
        printWithThread("Start CompletableFutureExample...");

        CompletableFuture.supplyAsync(() -> {
            printWithThread("Inside supplyAsync");
            if (System.currentTimeMillis() % 2 == 0) {
                throw new RuntimeException("Even time...");     // 50% chance to fail
            }
            return "Winter is Coming!";
        }).thenAcceptAsync(s -> {
            printWithThread("thenAcceptAsync: " + s);
        }).exceptionally(e -> {
            printWithThread("exceptionally: " + e.getMessage());
            return null;
        });

        Thread.sleep(500);  // waiting for full response
        printWithThread("...End");
    }

    private static void printWithThread(String desc) {
        System.out.printf("[%s] - %s%n", Thread.currentThread().getName(), desc);
    }
}
                    

If it's odd number, no RuntimeException raised, everything work as per "normal":

[main] - Start CompletableFutureExample...
[ForkJoinPool.commonPool-worker-3] - Inside supplyAsync
[ForkJoinPool.commonPool-worker-3] - thenAcceptAsync: Winter is Coming!
[main] - ...End

But if it's even number, RuntimeException raised, exceptionally method is called:

[main] - Start CompletableFutureExample...
[ForkJoinPool.commonPool-worker-3] - Inside supplyAsync
[main] - exceptionally: java.lang.RuntimeException: Even time...
[main] - ...End

As per our example, exceptionally will be invoked in the main thread.

CompletionStage::exceptionallyAsync

Two new methods to handle exception asynchronously:

Let's check following example that using exceptionallyAsync without Executor:

CompletableFutureExampleAsync.java
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExampleAsync {

    public static void main(String[] args) throws Exception {
        printWithThread("Start CompletableFutureExampleAsync...");

        CompletableFuture.supplyAsync(() -> {
            printWithThread("Inside supplyAsync");
            if (System.currentTimeMillis() % 2 == 0) {
                throw new RuntimeException("Even time...");     // 50% chance to fail
            }
            return "Winter is Coming!";
        }).thenAcceptAsync(s -> {
            printWithThread("thenAcceptAsync: " + s);
        }).exceptionallyAsync(e -> {
            printWithThread("exceptionallyAsync: " + e.getMessage());
            return null;
        });

        Thread.sleep(500);  // waiting for full response
        printWithThread("...End");
    }

    private static void printWithThread(String desc) {
        System.out.printf("[%s] - %s%n", Thread.currentThread().getName(), desc);
    }
}
                    

[main] - Start CompletableFutureExampleAsync...
[ForkJoinPool.commonPool-worker-3] - Inside supplyAsync
[ForkJoinPool.commonPool-worker-3] - exceptionallyAsync: java.lang.RuntimeException: Even time...
[main] - ...End

We can see that the exceptionallyAsync is invoked in the same thread as supplyAsync. Let's check below example for exceptionallyAsync with Executor:

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

public class CompletableFutureExampleAsyncExecutor {

    public static void main(String[] args) throws Exception {
        printWithThread("Start CompletableFutureExampleAsyncExecutor...");

        ExecutorService executor = Executors.newFixedThreadPool(10);
        CompletableFuture.supplyAsync(() -> {
            printWithThread("Inside supplyAsync");
            if (System.currentTimeMillis() % 2 == 0) {
                throw new RuntimeException("Even time...");     // 50% chance to fail
            }
            return "Winter is Coming!";
        }).thenAcceptAsync(s -> {
            printWithThread("Result: " + s);
        }).exceptionallyAsync(e -> {
            printWithThread("exceptionallyAsync: " + e.getMessage());
            return null;
        }, executor
        ).thenApply(s -> {
            printWithThread("Inside thenApply");
            return "The Winds of Winter!";
        }).thenAccept(CompletableFutureExampleAsyncExecutor::printWithThread);

        Thread.sleep(500);  // waiting for full response
        printWithThread("...End");
    }

    private static void printWithThread(String desc) {
        System.out.printf("[%s] - %s%n", Thread.currentThread().getName(), desc);
    }
}
                    

[main] - Start CompletableFutureExampleAsyncExecutor...
[ForkJoinPool.commonPool-worker-3] - Inside supplyAsync
[pool-1-thread-1] - exceptionallyAsync: java.lang.RuntimeException: Even time...
[pool-1-thread-1] - Inside thenApply
[pool-1-thread-1] - The Winds of Winter!
[main] - ...End

Per our example, with Executor the exceptionallyAsync will be invoked in the new thread.

exceptionallyCompose and exceptionallyComposeAsync

The next three methods that when this stage completes exceptionally, will returns a new CompletionStage that is composed using the results of the supplied function applied to this stage's exception.

This gives us more tools to recover from all the things that can break out there, as per next sample:

CompletableFutureExampleComposeAsync.java
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExampleComposeAsync {

    public static void main(String[] args) throws Exception {
        printWithThread("Start CompletableFutureExampleComposeAsync...");

        CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
            printWithThread("Inside CF1 supplyAsync");
            if (System.currentTimeMillis() % 2 == 0) {
                throw new RuntimeException("Even time...");  // 50% chance to fail
            }
            return "Winter is Coming!";
        });

        CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
            printWithThread("Inside CF2 supplyAsync");
            return "The Winds of Winter!";
        });
        
        CompletableFuture<String> excCompose = cf1.exceptionallyComposeAsync(e -> {
            printWithThread("exceptionally: " + e.getMessage());
            return cf2;
        });
        
        excCompose.thenAcceptAsync(s -> {
            printWithThread("thenAcceptAsync: " + s);
        });

        Thread.sleep(500);  // waiting for full response
        printWithThread("...End");
    }

    private static void printWithThread(String desc) {
        System.out.printf("[%s] - %s%n", Thread.currentThread().getName(), desc);
    }
}
                    

If no RuntimeException, here the output:

[main] - Start CompletableFutureExampleComposeAsync...
[ForkJoinPool.commonPool-worker-5] - Inside CF2 supplyAsync
[ForkJoinPool.commonPool-worker-3] - Inside CF1 supplyAsync
[ForkJoinPool.commonPool-worker-3] - thenAcceptAsync: Winter is Coming!
[main] - ...End

When RuntimeException happen:

[main] - Start CompletableFutureExampleComposeAsync...
[ForkJoinPool.commonPool-worker-3] - Inside CF1 supplyAsync
[ForkJoinPool.commonPool-worker-5] - Inside CF2 supplyAsync
[ForkJoinPool.commonPool-worker-5] - exceptionally: java.lang.RuntimeException: Even time...
[ForkJoinPool.commonPool-worker-3] - thenAcceptAsync: The Winds of Winter!
[main] - ...End