During software engineering there is often a need to handle tasks that run in the background and might fail. Using CompletableFuture helps with running tasks asynchronously, and Try from the FunctionalUtils library helps manage errors in a functional way. But combining these can make the code complex. This article introduces TryT, a special tool that wraps Try inside CompletableFuture. This makes it easier to handle both asynchronous tasks and errors together.

For an introduction to functional programming you can read here

What is a Monad Transformer?

A monad transformer combines two monads, allowing you to work with both at the same time without getting confused. If you have a CompletableFuture for asynchronous tasks and a Try for handling errors, a monad transformer like TryT wraps them together so you can manage both effects more easily.

What is TryT?

TryT is a tool that combines Try and CompletableFuture. It helps you handle tasks that run in the background and might fail. It makes it simpler to chain these tasks and manage errors in a clean way. The name follows the naming conventions used by functional libraries in regards with monad transformers by adding a T suffix.

Why Use TryT?

Directly working with CompletableFuture<Try<T>> can make your code complex and hard to read. TryT simplifies this by:

  1. Combining Error and Async Handling: It handles both errors and asynchronous tasks together.
  2. Cleaner Code: Makes your code easier to read and maintain.
  3. Easier to Chain Tasks: Helps you chain tasks without writing a lot of extra code.

Examples

  1. Using CompletableFuture<Try<T>> directly:

CompletableFuture<Try<String>> futureTry = someAsyncOperation();

CompletableFuture<Try<Integer>> result = futureTry.thenApply(tryValue -> {
    return tryValue.map(String::length);
});

Whereas with TryT:


TryT.fromFuture(someAsyncOperation())
    .map(String::length)
  1. Chaining Asynchronous Operations

CompletableFuture<Try<String>> futureTry = someAsyncOperation();

CompletableFuture<Try<Integer>> result = futureTry.thenCompose(tryValue -> {
    if (tryValue.isSuccess()) {
        return someOtherAsyncOperation(tryValue.get())
            .thenApply(Try::success)
            .exceptionally(Try::failure);
    } else {
        return CompletableFuture.completedFuture(Try.failure(tryValue.getCause()));
    }
});

Whereas with TryT:


TryT.fromFuture(someAsyncOperation())
    .flatMap(value -> TryT.fromFuture(someOtherAsyncOperation(value)));
  1. Error recovery

CompletableFuture<Try<String>> futureTry = someAsyncOperation();

CompletableFuture<Try<String>> recovered = futureTry.thenApply(tryValue -> {
    return tryValue.recover(ex -> "Fallback value");
});

Whereas with TryT:


TryT<String> tryT = TryT.fromFuture(someAsyncOperation());

TryT<String> recovered = tryT.recover(ex -> "Fallback value");

Conclusion

The TryT monad transformer helps you manage asynchronous tasks and errors together in a simpler way. By combining Try with CompletableFuture, TryT provides a clean and functional approach to handle both errors and asychronous tasks. This makes your code easier to read and maintain.