Search code examples
javacompletable-future

Add a description to CompletableFuture orTimeout


What is the easy way to add some addition info into the timeout exception? Suppose I have a code like that:

CompletableFuture<ResutType> getSomething()
{
  ... return someFuture.orTimeout(15, SECONDS);
}

This is all I'm getting currently downstream later on when callers of the method trying to do get():

java.util.concurrent.ExecutionException: java.util.concurrent.TimeoutException
    at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:396)
    at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2073)

I need to add more info - which operation has timed out, how long timeout was, etc.. In other words I need some message to the exception like "The Something timed-out in 15s", ideally use a supplier like:

CompletableFuture<ResutType> getSomething()
{
   return someFuture.orTimeout(15, SECONDS, () -> new TimeoutException("The Something timed-out in 15s"));
}

Solution

  • You can use exceptionallyCompose (or any of the async variants of this method) for that:

    public final class Example {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
                        sleep(3000);
                        return "DONE";
                    })
                    .orTimeout(1000, TimeUnit.MILLISECONDS)
                    .exceptionallyCompose(throwable -> {
                        if (throwable instanceof TimeoutException) {
                            var tex = new TimeoutException("The Something timed-out in 15s");
                            return CompletableFuture.failedFuture(tex);
                        }
                        return CompletableFuture.failedFuture(throwable);
                    });
    
            sleep(2000);
            future.get();
            sleep(5000);
        }
    
        private static void sleep(long ms) {
            try {
                Thread.sleep(ms);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    

    If your code fails due to a TimeoutException, you can return a TimeoutException (or any other throwable) that has more detail via a failed CompletableFuture. Otherwise, you can just return the same throwable that caused one of the previous stages to be completed exceptionally.

    For example, if there had been some RuntimeException in supplyAsync, future.get() would still cause a RuntimeException (wrapped within an ExecutionException) to be thrown. If there is no exception (i.e. also no timeout), you get() your result normally.

    With that knowledge you can write your own little orTimeout wrapper, for example:

    public static <T> CompletableFuture<T> orTimeout(
            CompletableFuture<T> cf,
            long timeout, TimeUnit unit
    ) {
        return cf.orTimeout(timeout, unit).exceptionallyCompose(throwable -> {
            if (throwable instanceof TimeoutException) {
                final var msg = String.format(
                        "The Something timed-out in %d %s.",
                        timeout, unit
                );
                return CompletableFuture.failedFuture(new TimeoutException(msg));
            }
            return CompletableFuture.failedFuture(throwable);
        });
    }
    

    and then use it for all CompletableFutures like so:

    CompletableFuture<String> future = orTimeout(CompletableFuture.supplyAsync(() -> {
        sleep(10000);
        return "DONE";
    }), 1000, TimeUnit.MILLISECONDS);