Java’s CompletableFuture and typed exception handling
With version 8, Java finally jumped on the asynchronous programming bandwagon with its own Promise-Oriented programming model, implemented by the CompletableFuture
class and a set of interfaces and implementations it uses. The model is generally useful and not as horribly complicated as we sometimes get in the Java foundation class library1, and it lends itself to fluent programming much better than the comparable model from fluent API proponent Vert.x project.
The Problem
One thing that most asynchronous computing models suffer from – and Java’s CompletableFuture
is no exception – is the loss of typed exception handling. While CompletableFuture.exceptionally()
is a good model that does not introduce a lot of boilerplate2, you do lose the ability of the try..catch..finally
syntax to effortlessly ignore exceptions you are not ready to handle and just letting them propagate up the stack.
With CompletableFuture.exceptionally()
, and similar implementations, if you want to add error handling to your code, by adding an exceptionally() step you are required to handle all errors, or – if you want to be selective about it – manually compare types and wrap whatever you don’t handle in a RuntimeException to propagate them downstream3 (and force the downstream to unwrap the exceptions to have a chance of understanding error types).
Consider the following (common) example:
public CompletableFuture<Pojo> getValueFromService(String param) { return getAPIClient() .thenCompose(api -> api.getValue(param)) .exceptionally(t -> { while (t instanceof RuntimeException && Objects.nonNull(t.getCause())) { t = t.getCause(); // unwrap wrapped checked exceptions } if (t instanceof InvalidParamException) { // convert "no such param" to null, which is what the client *really* meant return null; } if (t instanceof ParamNotSetException) { // "param not set" helpfully provides a default value, just in an icky way return ( (ParamNotSetException) t).getDefaultValue(); } // lather-rinse-repeat for any other typed handling // let downstream "know" that unexpected bad things happened throw new RuntimeException(t); }); }
Pretty unfriendly, right? The error handling code is several times the size of the actual program code.
And then the caller to getValueFromService()
has to chain an exceptionally()
of its own that need to start by unwrapping the RuntimeException
(that is if they even want to handle all the mess that is typed exception handling, instead of just WTFing it and going back to the Javascript-style “an error happened, I couldn’t be bothered to try to recover, sucks to be you“).
A Solution
Suppose for a minute that exceptionally()
had a two parameter overload, that accepted an Exception class as a first parameter and a lambda that accepts an error of that type as a second parameter. Wouldn’t that be more useful?
Lets try:
public CompletableFuture<Pojo> getValueFromService(String param) { return getAPIClient() .thenCompose(api -> api.getValue(param)) .exceptionally(InvalidParamException.class, // convert "no such param" to null, which is what the client *really* meant e -> null) .exceptionally(ParamNotSetException.class, // "param not set" helpfully provides a default value, just in an icky way ParamNotSetException::getDefaultValue); }
Isn’t this so much nicer? No need to wrap exceptions any more (and so no need to unwrap them) and promise-oriented error recovery looks really nice – much nicer than the try..catch..finally
syntax it replaces.
Unfortunately this API doesn’t exist, and the CompletableFuture
class doesn’t lend itself to extending very well – I tried and the result wasn’t pretty. Extending the class is much easier in Java 9 and there is even an example child class in the documentation (also the missing failedFuture()
method was added in Java 9).
As an alternative to the missing API, I wrote a helper class that allows you to get the same logic with just a bit more text (in Java 9 you’d probably want to add that to a class that extends CompletableFuture
). The crux of the implementation is this method:
public static <T,E extends Throwable> Function<Throwable, ? extends T> on( Class<E> errType, Function<E, ? extends T> fn) { return t -> { if (!errType.isInstance(t)) Thrower.spit(t); // helper method to throw undeclared checked exceptions @SuppressWarnings("unchecked") E e = (E)t; return fn.apply(e); }; }
It basically just implements the type test before calling the lambda function with the correct type, and spits unmatched exceptions downstream for handling. Wrap it all up in some generics and you get a nice and friendly API. See this article for a discussion on throwing undeclared checked exceptions and see the full implementation for details.
Then using is not more difficult then the wishful thinking demonstrated above:
public CompletableFuture<Pojo> getValueFromService(String param) { return getAPIClient() .thenCompose(api -> api.getValue(param)) .exceptionally(Futures.on(InvalidParamException.class, // convert "no such param" to null, which is what the client *really* meant e -> null)) .exceptionally(Futures.on(ParamNotSetException.class, // "param not set" helpfully provides a default value, just in an icky way ParamNotSetException::getDefaultValue)); }
See the full code for the exceptionally()
helper, including the helper Thrower
in this gist.
Would you like to see this feature in Java 10? Leave your comments here, on the gist, or in your favorite JCP.
- especially for things that claim to be “enterprise versions”, aughh [↩]
- again, compare to Vert.x
AsyncResult
handlers. Other APIs, such as RX also do a good job in reducing boilerplate around error handling [↩] - “downstream” is the promise-oriented term for what procedural programming calls “up the execution stack” [↩]
Thanks for this informative read, I have shhared
itt on Twitter.