sakib.ninja sakib.ninja

Java: Asynchronous programming 101

Aug 31, 2025 3 min read #java#concurrency

In modern software development, the ability to handle multiple tasks simultaneously is becoming increasingly important. Asynchronous programming, which allows a program to continue executing while waiting for a long-running task to complete, is an essential technique for achieving this. In this blog post, we will explore asynchronous programming in Java using the CompletableFuture class.

What is CompletableFuture?

CompletableFuture is a class introduced in Java 8 that represents a future result of an asynchronous computation. It can be used to execute tasks asynchronously and return a result when the task completes. CompletableFuture provides several methods to compose, combine and transform its results, which makes it a powerful tool for writing asynchronous code.

To create a CompletableFuture, we can use the static factory methods in the CompletableFuture class. For example, to create a CompletableFuture that returns a string, we can use the following code:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // some long-running computation
    return "result";
});

This code creates a CompletableFuture that executes a long-running computation asynchronously and returns a string result when the computation completes.

When a CompletableFuture completes, we can retrieve its result using the get method. However, calling the get method blocks the current thread until the result is available. A better way to handle the result is to use the thenAccept method, which allows us to specify a callback function that is invoked when the CompletableFuture completes.

For example, the following code shows how to use the thenAccept method to print the result of the CompletableFuture:

public CompletableFuture<Integer> externalApiCall() {
        return CompletableFuture.supplyAsync(() -> {
            int val = new Random().nextInt(100);

            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return val;
        });
}

externalApiCall()
.thenAccept(val -> System.out.println("Returned Value: " + val));

We can chain multiple CompletableFutures together to execute a series of asynchronous tasks. When one CompletableFuture completes, we can use its result to start the next CompletableFuture. We can chain CompletableFutures using the thenCompose or thenCombine method. Example,

public CompletableFuture<Integer> externalApiCall() {
        return CompletableFuture.supplyAsync(() -> {
            int val = new Random().nextInt(100);

            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return val;
        });
}

externalApiCall()
// Converting Integer to String using thenCompose
.thenCompose(val -> CompletableFuture.completedFuture(String.valueOf(val)))
.thenAccept(val -> System.out.println("Returned Value: " + val));

externalApiCall()
// Returns sum of first and 2nd CompletableFuture by thenCombine
.thenCombine(externalApiCall(), Integer::sum)
.thenAccept(val -> System.out.println("Returned Value: " + val));

Besides, CompletableFuture provides several methods for handling exceptions that may occur during the execution of an asynchronous task. We can use the exceptionally method to specify a callback function that is invoked when an exception occurs. We can also use the handle method to specify a callback function that is invoked regardless of whether the CompletableFuture completes normally or exceptionally.

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    // some long-running computation that may throw an exception
    throw new RuntimeException("exception");
});

future.exceptionally(ex -> {
    System.out.println("Exception: " + ex.getMessage());
    return 0;
});

future.thenAccept(result -> System.out.println("Result: " + result);

That’s it.