简体   繁体   中英

CompletableFuture swallows exceptions?

I've been playing around with CompletableFuture and noticed a strange thing.

String url = "http://google.com";

CompletableFuture<String> contentsCF = readPageCF(url);
CompletableFuture<List<String>> linksCF = contentsCF.thenApply(_4_CompletableFutures::getLinks);

linksCF.thenAccept(list -> {
    assertThat(list, not(empty()));
});

linksCF.get();

If, in my thenAccept call, the assertion fails, the exception is not propagated. I tried something even uglier then:

linksCF.thenAccept(list -> {
    String a = null;
    System.out.println(a.toString());
});

nothing happens, no exception is propagated. I tried using methods like handle and others related to exceptions in CompletableFutures , but failed - none is propagating the exception as expected.

When I debugged the CompletableFuture , it does catch the exception like this:

final void internalComplete(T v, Throwable ex) {
    if (result == null)
        UNSAFE.compareAndSwapObject
            (this, RESULT, null,
             (ex == null) ? (v == null) ? NIL : v :
             new AltResult((ex instanceof CompletionException) ? ex :
                           new CompletionException(ex)));
    postComplete(); // help out even if not triggered
}

and nothing else.

I'm on JDK 1.8.0_05 x64, Windows 7.

Am I missing something here?

The problem is you never request to receive the results of your call to linksCF.thenAccept(..) .

Your call to linksCF.get() will wait for the results of the execution in your chain. But it will only return the results of then linksCF future. This doesn't include the results of your assertion.

linksCF.thenAccept(..) will return a new CompletableFuture instance. To get the exception thrown call get() or check the exception status with isCompletedExceptionally() on the newly return CompletableFuture instance.

CompletableFuture<Void> acceptedCF = linksCF.thenAccept(list -> {
    assertThat(list, not(empty()));
});

acceptedCF.exceptionally(th -> {
    // will be executed when there is an exception.
    System.out.println(th);
    return null;
});
acceptedCF.get(); // will throw ExecutionException once results are available

Alternative?

CompletableFuture<List<String>> appliedCF = linksCF.thenApply(list -> {
    assertThat(list, not(empty()));
    return list;
});

appliedCF.exceptionally(th -> {
    // will be executed when there is an exception.
    System.out.println(th);
    return Coolections.emptyList();
});
appliedCF.get(); // will throw ExecutionException once results are available

Although the question is basically already answered by Gregor Koukkoullis (+1), here is a MCVE that I created to test this.

There are several options for obtaining the actual exception that caused the problem internally. However, I don't see why calling get on the future that is returned by thenAccept should be an issue. In doubt, you could also use thenApply with the identity function and use a nice fluent pattern, like in

List<String> list = 
    readPage().
    thenApply(CompletableFutureTest::getLinks).
    thenApply(t -> {
        // check assertion here
        return t;
    }).get();

But maybe there's a particular reason why you want to avoid this.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Supplier;

public class CompletableFutureTest
{
    public static void main(String[] args) 
        throws InterruptedException, ExecutionException
    {
        CompletableFuture<String> contentsCF = readPage();
        CompletableFuture<List<String>> linksCF = 
            contentsCF.thenApply(CompletableFutureTest::getLinks);

        CompletableFuture<Void> completionStage = linksCF.thenAccept(list -> 
        {
            String a = null;
            System.out.println(a.toString());
        });        

        // This will NOT cause an exception to be thrown, because
        // the part that was passed to "thenAccept" will NOT be
        // evaluated (it will be executed, but the exception will
        // not show up)
        List<String> result = linksCF.get();
        System.out.println("Got "+result);


        // This will cause the exception to be thrown and
        // wrapped into an ExecutionException. The cause
        // of this ExecutionException can be obtained:
        try
        {
            completionStage.get();
        }
        catch (ExecutionException e)
        {
            System.out.println("Caught "+e);
            Throwable cause = e.getCause();
            System.out.println("cause: "+cause);
        }

        // Alternatively, the exception may be handled by
        // the future directly:
        completionStage.exceptionally(e -> 
        { 
            System.out.println("Future exceptionally finished: "+e);
            return null; 
        });

        try
        {
            completionStage.get();
        }
        catch (Throwable t)
        {
            System.out.println("Already handled by the future "+t);
        }

    }

    private static List<String> getLinks(String s)
    {
        System.out.println("Getting links...");
        List<String> links = new ArrayList<String>();
        for (int i=0; i<10; i++)
        {
            links.add("link"+i);
        }
        dummySleep(1000);
        return links;
    }

    private static CompletableFuture<String> readPage()
    {
        return CompletableFuture.supplyAsync(new Supplier<String>() 
        {
            @Override
            public String get() 
            {
                System.out.println("Getting page...");
                dummySleep(1000);
                return "page";
            }
        });
    }

    private static void dummySleep(int ms)
    {
        try
        {
            Thread.sleep(ms);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }
}

If, in my thenAccept call, the assertion fails, the exception is not propagated.

The continuation that you register with thenAccept() is a separate task from the linksCF future. The linksCF task completed successfully; there is no error for it to report. It has its final value. An exception thrown by linksCF should only indicate a problem producing the result of linksCF ; if some other piece of code that consumes the result throws, that does not indicate a failure to produce the result.

To observe an exception that happens in a continuation, you must observe the CompletableFuture of the continuation.

correct. but 1) I should not be forced to call get() - one of the points of the new constructs; 2) it's wrapped in an ExecutionException

What if you wanted to hand the result off to multiple, independent continuations using thenAccept() ? If one of those continuations were to throw, why should that impact the parent, or the other continuations?

If you want to treat linksCF as a node in a chain and observe the result (and any exceptions) that happen within the chain, then you should call get() on the last link in the chain.

You can avoid the checked ExecutionException by using join() instead of get() , which will wrap the error in an unchecked CompletionException (but it is still wrapped).

The answers here helped me to manage exception in CompletableFuture, using "exceptionnaly" method, but it missed a basic example, so here is one, inspired from Marco13 answer:

/**
 * Make a future launch an exception in the accept.
 *
 * This will simulate:
 *  - a readPage service called asynchronously that return a String after 1 second
 *  - a call to that service that uses the result then throw (eventually) an exception, to be processed by the exceptionnaly method.
 *
 */
public class CompletableFutureTest2
{
    public static void main(String[] args)
        throws InterruptedException, ExecutionException
    {
        CompletableFuture<String> future = readPage();

        CompletableFuture<Void> future2 = future.thenAccept(page->{
            System.out.println(page);
            throw new IllegalArgumentException("unexpected exception");
        });

        future2.exceptionally(e->{
          e.printStackTrace(System.err);
          return null;
        });

    }

    private static CompletableFuture<String> readPage()
    {

      CompletableFuture<String> future = new CompletableFuture<>();
      new Thread(()->{
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
        }

        // FUTURE: normal process
        future.complete("page");

      }).start();
        return future;
    }


}

The mistake to avoid is to call "exceptionnaly" on the 1st future (the variable future in my code) instead of the future returned by the "thenAccept" which contains the lambda that may throw an exception (the variable future2 in my code). .

As usual, understanding the behavior of CompletableFuture is better left to the official docs and a blog.

Each then...() chaining method of the CompletableFuture class, which implements CompletionStage , accepts a an argument a CompletionStage . The stage that is passed depends on which order of then...() methods you've chained. Again, docs, but here's that aforementioned blog .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM