Introduction to CompletableFutures
CompletableFuture
is a new addition in Java 1.8. It's a type of Future
object allowing execution of code upon completion. To explain the idea, let's first recap what a future object is.
A future, or promise as it's sometimes called in other languages, is an object that represents future result of an action. Rather than returning the result immediately, a future is returned which will contain the result at a later point. Consider for instance a get operation on a database. The database is a different process on the server, or on a different server. The database API library has to send the request to the database, and wait for the result. The method for the get request might look something like public DbResult get(String key)
. This function is blocking. This means means when we call get()
, the API will wait until he result is available before returning it.
The thread will sit idle while the database is processing the request. Other threads can of course continue to execute, but we could be doing other things while we wait for the database.
One solution would be a future object. We change the method signature to public Future<DbResult> get(String key)
. Now the API library immediately returns a future object. We can query the future object later to check if it has completed:
Future<DbResult> future = dbLib.get("mykey");
// get() returns immediately, and the result may not yet be available.
// Do other things while the database is executing.
// ...
// We now need the result from the database, wait for it.
DbResult result = future.get();
The advantage of using the future object is other things can be done while we wait for external resources. This means lower response times and better resource utilisation. However, there is still a problem: future.get()
blocks. There are ways to inspect future objects to see if they're done, but that's beyond the scope of this article. In the end we still have to wait for the result to become available. What if there was a way we could tell the future object to execute some code whenever its ready?
Enter CompletableFuture
. If you're familair with JavaScript, you may already be familiar with "promises" or "thenables". My personal favourite is the Q library, which implements an idea like CompletableFuture
. Consider the database example from above:
CompletableFuture<DbResult> future = dbLib.get("mykey");
// Again, get() returns immediately.
// We then set up future processing.
future.thenAccept(result -> {
// Process the result.
});
// Carry on doing other things which don't depend on the database result.
The completable future calls thenAccept()
as soon it's completed. This is one of the many ways to interact with completable futures. There is no longer any need to block the current thread to wait for anything. There are a few other then...()
methods. Before diving into some of them in detail, here's a quick summary of the most useful ones:
Method | Async method | Arguments | Returns |
---|---|---|---|
thenAccept() | thenAcceptAsync() | Result of previous stage | Nothing |
thenRun() | thenRunAsync() | None | Nothing |
thenApply() | thenApplyAsync() | Result of previous stage | Result of current stage |
thenCompose() | thenComposeAsync() | Result of previous stage | Future result of current stage |
thenCombine() | thenCombineAsync() | Result of two previous stages | Result of current stage |
whenComplete() | whenCompleteAsync() | Result or exception from previous stage | Nothing |
When the future completes
The first method we will examine is thenAccept()
. We touched on this method in the previous example:
System.out.println("Requesting");
get("mykey").thenAccept(result -> {
System.out.println("Processing");
});
System.out.println("Requested");
This will print:
Requesting
Requested
Processing
As we see, get()
returns immediately. The call to thenAccept()
tells the future we want more work done when the result is ready. I should note here that thenAccept()
itself returns a CompletableFuture<Void>
. Later we will cover how to chain many futures together in stages.
Transforming a result
thenApply()
is like thenAccept()
, except it returns a CompletableFuture
with a result rather than Void
. The result will be the object returned from inside the callback. Imagine a parse()
method that looks like this, taking a string and parsing it into a Person
:
public Person parse(String data) {
Person person = new Person();
// Parse data and set values on person.
return person;
}
Now let's use this code within thenApply()
, to create a person object from our database result:
CompletableFuture<Person> future = get(id).thenApply((result) -> {
// Result is a String. Send it to the parse method.
Person person = parse(result);
// Now return the Person back out.
return person;
});
get()
still returns a CompletableFuture<String>
, but using thenApply()
to transform the result, thenApply()
will itself return a CompletableFuture<Person>
. We can then use this future for something else later.
Creating futures
Before going further, let's examine how to create futures manually. This will help us to understand how to work with them.
The first way we will cover is how to synchronously return an object using a CompletableFuture
. This is useful when you are writing code to work asynchronously, but for whatever reason you can return the result immediately. For instance if you have a memory cache of fetched objects. If the object is not cached we do a remote fetch which we have to wait for. However, if the object is cached, we can simply return it immediately. We can easily wrap objects within futures by using the static method CompletableFuture.completedFuture(obj)
. This returns an already completed CompletableFuture
with obj
as the result.
The other way is to construct a CompletableFuture
and manually set the result on it when the result is available. We talked about a caching method using both approaches in tandem, so let's see an example of that:
public CompletableFuture<Person> get(String id) {
// When creating future manually, it's important to include try/catch blocks.
// This is because when an exception is thrown, we want to wrap it in a failed future.
try {
// cache is a simple Map<String, Person>.
Person cached = cache.get(id);
// If we have the object cached, simply wrap and return it.
if (cached != null) {
return CompletableFuture.completedFuture(cached);
}
// Object was not cached, create CompletableFuture to represent its future availability.
CompletableFuture<Person> future = new CompletableFuture<>();
// Code to fetch the Person here.
// This example uses a legacy callback class as an example, but it could be any type of asynchronous code.
databaseFetch(id, person -> {
// Cache the person. (this step is specific to this example, included for completeness)
cache.put(id, person);
// Send the result to the future.
future.complete(person);
});
// Return the future.
return future;
}
// Wrap all errors in a failed future.
catch (Throwable e) {
// If errors happen, we use completeExceptionally() to report them.
// The get() method itself should not throw any exceptions.
CompletableFuture<Person> future = new CompletableFuture<>();
future.completeExceptionally(e);
return future;
}
}
As we can see, get()
will always return a CompletableFuture
, regardless of whether the object was cached or not. Even if an exception occurred, it would still be wrapped in a CompletableFuture
. We will touch more on exception handling later, as it's an important topic.
Chaining other futures
We now know how to execute something asynchronously and process that result. How would we go about doing further asynchronous work once our first stage completes. Let's imagine we fetch an employee and also want the company that person is employed with. For this we use thenCompose()
, which provides the result of the previous stage, but expects another CompletableFuture
to be returned:
CompletableFuture<Person> personFuture = fetchPerson(id);
CompletableFuture<Company> companyFuture = personFuture.thenCombine(person -> {
return fetchCompany(person.getCompanyId());
});
CompletableFuture<Void> future = companyFuture.thenAccept(company -> {
// We now have the company.
});
This is a simple example, but notice in the callback on the companyFuture
we don't have a reference to person
anymore. For this we have to resort to inline chaining:
CompletableFuture<Void> future = fetchPerson(id).thenCompose(person -> {
return fetchCompany(person.getCompanyId()).thenAccept(company -> {
// Now we have a reference to both objects.
});
});
What happens here is fetchPerson()
returns a CompletableFuture<Person>
. We immediately call thenCompose()
on it, which returns the CompletableFuture
from fetchCompany()
. Once fetchCompany()
completes, our callback in the final thenAccept()
block is executed. This is a simple example of chaining futures, which we will cover in a bit. First we need to talk about error handling.
Error handling
When calling any of the asynchronous handler methods decribed in this article, you must make sure errors are dealt with. If you don't provide any handler code, they may very well get lost completely, and you might never see them. In order to catch exceptions, there are two things you can do.
The first, and correct, way is to call exceptionally
on CompletableFuture
. This provides a piece of code to execute in case of errors.
fetch(id).thenAccept((result) -> {
// Block A: do something with result.
}).exceptionally((error) -> {
// Block B: handle errors.
return null;
});
Now if fetch
completes successfully, block A will be execute. If an error occurs, block B will be executed and respond to the exception. You probably also noticed that the error codes returns null
. This is because exceptionally()
itself returns a completable future. Returning null
makes it a future without any result. However, you can return a result from exceptionally()
, much like how thenApply()
works.
The second way to handle error is using get()
:
String result = provider.fetch(id).get();
get()
is a blocking call which can will wait for the result to become available, just like on a normal Future
. Any exception will be thrown as normal. Because this is a blocking call, it should be used with caution. Personally, I only use it in testing.
Warning: Never break the chain
Chaining is an important concept. It is also important to always maintain the chains through the entire application. Take a moment to think about what the following two code blocks would output:
CompletableFuture<String> future = CompletableFuture.completedFuture("foo");
future = future.thenApply(str -> {
System.out.println("Stage 1: " + str);
return "bar";
});
future = future.thenApply(str -> {
System.out.println("Stage 2: " + str);
throw new RuntimeException();
});
future = future.thenApply(str -> {
System.out.println("Stage 3: " + str);
});
future.exceptionally(e -> {
System.out.println("Exceptionally");
});
and:
CompletableFuture<String> future = CompletableFuture.completedFuture("foo");
future.thenApply(str -> {
System.out.println("Stage 1: " + str);
return "bar";
});
future.thenApply(str -> {
System.out.println("Stage 2: " + str);
throw new RuntimeException();
});
future.thenApply(str -> {
System.out.println("Stage 3: " + str);
return "abc";
});
future.exceptionally(e -> {
System.out.println("Exceptionally");
return null;
});
The output of the first block is:
Stage 1: foo
Stage 2: bar
Exceptionally
The output of the second block is:
Stage 1: foo
Stage 2: foo
Stage 3: foo
Two quite horrible things happened here: the result from stage 1 and the RuntimeException
from stage 2 were both lost. The intended flow was disrupted, as stage 2 was supposed to receive "bar" and not "foo" as a result.
Consider a third code block where the future is not immediately completed:
CompletableFuture<String> future = new CompletableFuture<>();
future.thenApply(str -> {
System.out.println("Stage 1: " + str);
return "bar";
});
future.thenApply(str -> {
System.out.println("Stage 2: " + str);
throw new RuntimeException();
});
future.thenApply(str -> {
System.out.println("Stage 3: " + str);
return "abc";
});
future.exceptionally(e -> {
System.out.println("Exceptionally");
return null;
});
future.complete("foo");
This outputs:
Stage 3: foo
Stage 2: foo
Stage 1: foo
Now we have a situation where the original flow is executed out of order. This is in turn dependent on whether the future was completed or not. Not chaining futures properly results in strange side effects and difficult bugs.
Conclusion
This is by no means intended to be an exhaustive explanation of CompletableFuture
, but hopefully provides a good introduction to the subject. Building asynchronous, non-blocking applications is where a lot of software architectures are going. CompletableFuture
adds an important piece of the puzzle, which was missing up until now.