[concurrency-interest] Layered exception handling with CompletableFuture

Peter Levart peter.levart at gmail.com
Wed Aug 27 09:56:25 EDT 2014


On 08/27/2014 10:39 AM, Millies, Sebastian wrote:
> So I find myself wrapping all the Callables, and tunneling the exceptions with RuntimeException, as Doug Lea recommends.

The interesting part of javadoc for CompletionStage is the following:

- Two method forms support processing whether the triggering stage 
completed normally or exceptionally: Method /whenComplete/ allows 
injection of an action regardless of outcome, otherwise preserving the 
outcome in its completion. Method /handle/ additionally allows the stage 
to compute a replacement result that may enable further processing by 
other dependent stages. In all other cases, if a stage's computation 
terminates abruptly with an (unchecked) exception or error, then all 
dependent stages requiring its completion complete exceptionally as 
well, with a */CompletionException/* holding the exception as its cause.

So If you do the following:

         CompletableFuture.supplyAsync(() -> {
             throw new IllegalStateException();
         }).whenComplete((x, t) -> {
             System.out.println("Result: " + x + "\nException: " + t);
         });

...you will get:

Result: null
Exception: java.util.concurrent.CompletionException: 
java.lang.IllegalStateException

So if stage funcition/supplier throws an (unchecked) exception, it is 
always wrapped with CompletionException as stage outcome. The signature 
of exception handling functions passed to CompletionStage methods 
declares plain Throwable, but at least CompletableFuture seems to always 
arange so that CompletionException is passed to those functions unless 
you call methods like completeExceptionally on it. You can use this to 
your advantage (will get to that shortly)...

Another "implementation detail" of CompletableFuture is evident if you 
try this:

         CompletableFuture.supplyAsync(() -> {
             throw new CompletionException(new Exception("I'm checked 
exception"));
         }).whenComplete((x, t) -> {
             System.out.println("Result: " + x + "\nException: " + t);
         });

...you will get:

Result: null
Exception: java.util.concurrent.CompletionException: 
java.lang.Exception: I'm checked exception

So if stage funcition/supplier throws CompletionException, it is not 
wrapped with another CompletionException as the stage outcome but it 
becomes the stage outcome directly.

Finally the following "implementation detail" of CompletableFuture is 
also useful for our purpose. If you try:

         try {
             CompletableFuture.supplyAsync(() -> {
                 throw new CompletionException(new Exception("I'm 
checked exception"));
             }).get();
         } catch (ExecutionException | InterruptedException e) {
             System.out.println("Got: " + e);
         }

...you will get:

Got: java.util.concurrent.ExecutionException: java.lang.Exception: I'm 
checked exception

So if CompletableFuture outcome is CompletionException wrapping some 
Throwable, then calling get() on the future will throw 
ExecutionException wrapping this same Throwable. The cause of 
CompletionException is effectively unwrapped before being wrapped with 
ExecutionException in get().

This behaviour lends itself to the use of *CompletionException* as a 
general wrapper for any exception (checked or unchecked) thrown by the 
stage function/supplier.

You can create helper functional interfaces:

     interface SupplierX<T> {
         T get() throws Throwable;
     }

     interface FunctionX<T, R> {
         R apply(T t) throws Throwable;
     }

     // ... etc ... and utility static methods:

     static <T> Supplier<T> wrap(SupplierX<T> supplier) {
         return () -> {
             try {
                 return supplier.get();
             } catch (Throwable e) {
                 throw new CompletionException(e);
             }
         };
     }

     static <R> Function<Throwable, R> unwrapWrap(FunctionX<Throwable, 
R> fn) {
         return (throwable) -> {
             try {
                 return fn.apply(throwable instanceof CompletionException
                                 ? throwable.getCause()
                                 : throwable);
             } catch (Throwable e) {
                 throw new CompletionException(e);
             }
         };
     }

     // ... etc.


And then use them as following:


         CompletableFuture<String> cf = CompletableFuture.supplyAsync(wrap(
             () -> {
                 double r = Math.random();
                 if (r < 1d / 3d) return "OK";
                 else if (r < 2d / 3d) throw new FileNotFoundException();
                 else throw new EOFException();
             }
         ));

         cf = cf.exceptionally(unwrapWrap(
             (t) -> {
                 try {
                     throw t;
                 } catch (IOException e) {
                     throw new IllegalStateException("Converted from: " 
+ e);
                 }
             }
         ));

         cf = cf.exceptionally(unwrapWrap(
             (t) -> {
                 try {
                     throw t;
                 } catch (RuntimeException e) {
                     return "Handled: " + e;
                 }
             }
         ));

         System.out.println(cf.get());



Regards, Peter

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://cs.oswego.edu/pipermail/concurrency-interest/attachments/20140827/40b605f3/attachment.html>


More information about the Concurrency-interest mailing list