Some time ago, over 2 years, I started a 3 part series on the CompletableFuture. I’m just now getting around to doing part two now. My long time delay in completing this series was due to working in my book Kafka Streams in Action. But now that’s done I can get back to doing some blogging again.

Earlier this year I started a series on a new class introduced in Java 8, the CompletableFuture class. Since the CompletableFuture is such a feature rich class, I decided to break the coverage up into three stages. The first post covered the creation of CompletableFuture tasks and how to specify followup tasks to execute when the original one completes. The examples in the first post only dealt with the happy path scenarios, however. Today we are going to go over dealing with failures and errors including specifying actions to take when an error is encountered.

Why Having Separate Error Handling Methods

CompletableFutures give you the ability to define functionality that can be executed then you can come back to it later and magically extract the result of your asynchronous task. But there is one drawback, what to do when errors occur? You can use try-catch blocks, but you lose the conciseness of lambda syntax. Plus you lose flexibility as you have to handle errors the same way, you can’t use a CompleteableFuture with error handling strategy A then five lines later use a different error handling strategy (assuming you are passing the same lambda with different parameters). What we need is a pluggable solution where different functions can be specified at any point to handle errors that best fit the specific situation.

Error Handling Strategies

When it comes to handling errors with Completeable futures, there are two approaches. The first is to run a given function when an Exception occurs. The second is a BiFunction with the expected result type and a [Throwable] as parameters. If an error occurred, the Throwable instance is not null and you can take the action at that point. While not error handling strategies, two methods will force an exception to be thrown when any attempt to call the CompletableFuture.get method is called. The two approaches differ in that the CompleteableFuture.excepionally is used when you don’t want to take any further action with the result, if the future completes normally, then the returned result is good enough. The only way the provided function executes is in the event of an error. But the case of CompletableFuture.handle method is different. If the future completes without error, the result is available for extra processing. Otherwise the Throwable parameter is not null and you can react at that point. The key point here is CompleteableFuture.handle method is always executed.

Functions That Run On Error

The first strategy for handling errors is the CompletableFuture.exceptionally method. The exceptionally method takes a Function that expects to receive an instance of Throwable and returns the same type of the original CompletebleFuture. If the case of normal completion, the result of the CompletableFuture is returned to the caller. But if there are any errors then the supplied function is executed, and that result is returned instead. //Code Here In other words CompleteableFuture.exceptionally only runs when there is an exception.

Handling Success or Failure

The second method we have for error handling is CompleteableFuture.handle. In contrast to exceptionally the handle method always executes the function parameter. We can see the difference in execution by the types the two methods accept. The exceptionally method requires a Function returning the same type as the CompletebleFuture. On the other hand, the handle method takes a BiFunction where the first parameter is the result of the CompleteableFuture computation and the second parameter is a //Code Here Throwable. So in our function, if the Throwable is not null we know an error occurred and took the appropriate action. Otherwise, we return the result of the CompleteableFuture. Or course we can perform additional operations on a successful result as long as we return the same type.

Choosing a Strategy

We have two strategies for asynchronous error handling, so the question is which type to use? While there are no hard rules here’s some quick advice:

  • When the result stands alone use CompleteableFuture.exceptional.
  • If the result requires more processing, use CompleteableFuture.handle instead.

Conclusion

We have reached the end of our coverage on CompletableFuture error handling. The takeaway(s) here are don’t add error handling inside your CompletableFuture. Instead, rely on the error handling process provided by the class. In next post on the CompleteableFuture, we’ll cover canceling and forcing completion.

Resources