Tiina
Tiina

Reputation: 4785

CompletableFuture exceptionally breaks the work chain

The idea of using CompletableFuture is because it offers a chain, while the first several steps encapsulate beans before the last step uses it. Because any exception may happen in these steps and exceptionally is used to handle error. However, exceptionally only accepts Throwable argument and so far I haven't found a way to grab those encapsulated beans.

CompletableFuture.supplyAsync(this::msgSource)
            .thenApply(this::sendMsg).exceptionally(this::errorHandler).thenAccept(this::saveResult)
public List<Msg> msgSource() // take message from somewhere.
public List<Msg> sendMsg(List<Msg>) // exceptions may happen like 403 or timeout
public List<Msg> errorHandler() // set a success flag to false in Msg.
public void saveResult(List<Msg>) // save send result like success or false in data center.

In the above example, comments are the working flow. However, since errorHandler neither accepts List<Msg> nor passes it on, so the chain is broken. How to get the return from msgSource?

EDIT

public class CompletableFutureTest {

    private static Logger log = LoggerFactory.getLogger(CompletableFutureTest.class);
    public static void main(String[] args) {
        CompletableFutureTest test = new CompletableFutureTest();
        CompletableFuture future = new CompletableFuture();
        future.supplyAsync(test::msgSource)
            .thenApply(test::sendMsg).exceptionally(throwable -> {
            List<String> list = (List<String>) future.join(); // never complete
            return list;
        }).thenAccept(test::saveResult);
        try {
            future.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    private List<String> saveResult(List<String> list) {
        return list;
    }

    private List<String> sendMsg(List<String> list) {
        throw new RuntimeException();
    }

    public List<String> msgSource() {
        List<String> result = new ArrayList<>();
        result.add("1");
        result.add("2");
        return result;
    }
}

enter image description here

Upvotes: 3

Views: 6595

Answers (1)

Holger
Holger

Reputation: 298153

A chain implies that each node, i.e. completion stage, uses the result of the previous one. But if the previous stage failed with an exception, there is no such result. It’s a special property of your sendMsg stage that its result is just the same value as it received from the previous stage, but that has no influence on the logic nor API design. If sendMsg fails with an exception, it has no result that the exception handler could use.

If you want to use the result of the msgSource stage in the exceptional case, you don’t have a linear chain any more. But CompletableFuture does allow to model arbitrary dependency graphs, not just linear chains, so you can express it like

CompletableFuture<List<Msg>> source = CompletableFuture.supplyAsync(this::msgSource);
source.thenApply(this::sendMsg)
      .exceptionally(throwable -> {
          List<Msg> list = source.join();
          for(Msg m: list) m.success = false;
          return list;
      })
      .thenAccept(this::saveResult);

However, there is no semantic difference nor advantage over

CompletableFuture.runAsync(() -> {
    List<Msg> list = msgSource();
    try {
        list = sendMsg(list);
    } catch(Throwable t) {
        for(Msg m: list) m.success = false;
    }
    saveResult(list);
});

which expresses the same logic as an ordinary code flow.

Upvotes: 3

Related Questions