An alternative to exceptions in Java: validations, part 2

(, en)

Validations are a concept to handle errors in a direct way. In essence, this means that errors can be treated as first class citizens and returned by a function. Suddenly they change from being a byproduct to being »into your face«.

Is this good? Sometimes yes and sometimes no.

It might get mazy.

Maze (Photo by Rayson Tan on Unsplash)

This post is a follow up of An alternative to exceptions in Java: validations, part 1, in case you feel lost, have a look there first.

At first glance validations offer a fresh view on error handling. You might think: now it is time to ditch exceptions and solve everything with validations. Well, if you have a hammer, … By trying to wrap all potentially erroneous constructs in validations, you might navigate yourself into tricky situations without an obvious solution. This post addresses these complex situations and gives advice on when not to use validations.

Validation Flows Figure 1: Validation flows. (a) A linear chain of validations is the sweet spot of validations. (b) Combining results from different validations into new validations, too. (c) Complexity arises from validations wrapped in other constructs such as CompletableFuture.

Where can it get “hairy”? Figure 1 gives an impression. But to set the base-line, let’s start with (a) and (b) first.

Vertical combination

Expressed in code, (a) would correspond to something like

String contractNumber = "23412324";
Validation<DomainError, Contract> contract = validateDefined(contractNumber)
        .flatMap(ValidationUtil::validateNumeric)
        .flatMap(ValidationUtil::validateSomethingElse)
        .map(this::getContract);

You combine the validations vertically, one result is the input of the next step. With list-like structures and some transformation, you can combine a list of validated items into a validated list of items.

List<Validation<DomainError, Tuple2<String, Contract>>> results1 = parse();
List<Validation<Seq<DomainError>, Tuple2<String, Contract>>> results2 = results1.map(v -> v.mapError(List::of));

Validation<Seq<DomainError>, Seq<Tuple2<String, Contract>>> sequence = Validation.sequence(results2);

Here the results from the parse() function return a Validation with a DomainError and a Tuple of the original contract number and the parsed ID. To actually flip the results to have the Validation enclosing the List, you have to use Validation.sequence(). One minor issue is that you have to turn your DomainError into Seq<DomainError> to be able to transform it. You are very lucky if you are on Java 11 or later, because the var keyword will reduce your line-length considerably.

Even better if you can put everything into one statement:

public Result getUserId(String mail) {
  return validateEmail(mail)
      .map(userService::findUser)
      //  signature of findUser: Option<User> findUser(String mail)
      .flatMap(maybeUser -> maybeUser.toValidation(new DomainError()))
      .map(User::getId)
      .fold(
          error -> status(Http.Status.BAD_REQUEST, error.toString()),
          id -> status(Http.Status.OK, id.toString())
      );
}

The nice thing about Vavr is that you can basically convert from one construct to the next without much hassle. In the example above, an Option returned by findUser() is converted into a Validation. The same also works with Try-constructs.

As a side note, sometimes it is a bit tricky to get what you want, e.g. a validation does not supply the error when .getOrElseThrow() is used. You have switch to an Either first:

validateEmail("abc@def.gh")
        // we cannot access the error part
        .getOrElseThrow(() -> new RuntimeException("could not validate"));

validateEmail("abc@def.gh")
        .toEither()
        // here we can access the error part
        .getOrElseThrow(error -> new RuntimeException(error.toString()));

Horizontal combination

Now, if you also want to combine multiple validations horizontally, you will fall into the category (b) of Figure 1.

Validation<DomainError, Address> address = validateAddress();
Validation<DomainError, String> email = validateEmail();
Validation<DomainError, String> phone = validatePhone();

address.combine(email).combine(phone)
        .ap((ad, ma, ph) -> new Contact(ad, ma, ph));

Validation.combine(address, email, phone)
        .ap((ad, ma, ph) -> new Contact(ad, ma, ph));

Validation.combine(address, email, phone)
        .ap(Contact::new);

Unfortunately Vavr does not supply a lot of documentation for the ap(fn) function, but you can find examples and posts about this topic quite easily online (e.g. here and here).

Wrap and unwrap

Last, but not least, we reach (c) of Figure 1: Resolving validations in the context of CompletableFuture- or nested validation constructs. These constructs can occur in larger systems that use a microservice architecture. Or in other words: with Lagom in combination with Play you’ll not be able to escape these constructs, if you want to use validations.

Suppose you need to check a user’s email address, extract the ID and then return his/her orders for an overview page:

public CompletionStage<Result> getOrders(String email) {

  Validation<DomainError, CompletionStage<User>> user =
      validateEmail(email).map(userService::getUser).map(ServiceCall::invoke);

  Validation<DomainError, CompletionStage<Seq<Order>>> orders =
      user.map(completedUser -> completedUser.thenApply(User::getId)
                  .thenCompose(id -> orderService.getOrders(id).invoke()));

  CompletionStage<Validation<DomainError, Seq<Order>>> ordersInsideOut =
      orders.fold(
          error -> CompletableFuture.completedFuture(Validation.invalid(error)),
          result -> result.thenApply(Validation::valid));

  // ... more service calls & validation steps ...

  // build the response
  return ordersInsideOut
      .thenApply(
          v -> v.fold(
            // mapping from DomainError to API error
            error -> badRequest(Json.toJson(toApiError(error))),
            // mapping from domain to API result,
            result -> ok(Json.toJson(toApi(result)))))
      .exceptionally(ex -> handleException1(ex));
}

The added complexity comes from having a CompletableFuture wrapping the results. My general guideline is to keep the validation close to the data, so naturally I would try to get the CompletableFuture to wrap the Validation and not the other way around. However, this really depends on the situation. If you go down the wrong route, you might get nasty constructs:

public CompletionStage<Result> getOrders2(String email) {
  CompletionStage<Validation<DomainError, User>> fold =
      validateEmail(email)
          .map(userService::getUser)
          .map(ServiceCall::invoke)
          .fold(
              error -> CompletableFuture.completedFuture(Validation.invalid(error)),
              result -> result.thenApply(Validation::valid));

  CompletionStage<Validation<DomainError, CompletionStage<Seq<Order>>>>
      validationCompletionStage =
          fold.thenApply(u -> u.map(User::getId))
              .thenApply(vid -> vid.map(id -> orderService.getOrders(id).invoke()));

  // do I want to continue? probably not
  throw new IDontWantToContinueAnymoreException();
}

Imagine you add some map(), flatMap() and ap() operations. Things might get even more interesting once two or more Lagom services have to be nested (one service needs validated input from the other service) and validated.

You’ll generate spaghetti code in no time.

In this case I would probably

  1. use exceptions instead of validations or
  2. split everything up into small functions

to make the error handling manageable.

Handling Exceptions

Depending on what framework you use, exceptions are an integral part of it. This means that they need to be caught and/or transformed into Validation-objects. You can use

If you have to deal with objects that need to be wrapped in a CompletableFuture,

be aware of the non-lazy nature of Java with functional programming. This can happen easily with CompletableFuture.completedFuture():

// executed at construction of CompletableFuture
// will NOT be caught by exceptionally
CompletableFuture.completedFuture(alwaysThrowException())
    .exceptionally(ex -> handleException(ex));

// executed in context of CompletableFuture
// will be caught by exceptionally
CompletableFuture.supplyAsync(() -> alwaysThrowException())
    .exceptionally(ex -> handleException(ex));

So only use CompletableFuture.completedFuture() if you are sure that the methods you call will not throw exceptions.

Conclusion

Validations give you a structured and consistent way of dealing with errors in a domain. This post is a tour through the main concepts and pitfalls. In summary, these guidelines proved useful to me:

Remember that Validations and Exceptions complement each other. At least in Java, you don’t have to choose one over the other.

Have fun!