An alternative to exceptions in Java: validations, part 1

(, en)

Exceptions in Java are inconsistent due to their special handling. They are like a separate flow of information, which not only claims extra resources of a developers mind, but also comes with a lot of boilerplate code and high likelihood of new bugs. Instead of exceptions, errors can be handled with »validations«. The underlying concept of a validation offers an effective and consistent approach to error handling and data validation. Even though validations are more than capable of handling any kind of exception, I will focus in this post on data and result validation.

This post is the first part of a two-part series. The second part dives into more messy situations and edge cases.

Patch the broken system

Why validations

A validation is a type that represents a (valid) result or an (invalid) error. It never contains both (a disjoint union); results are created by using Validation.valid and errors by using Validation.invalid. The Validation API in Vavr has a detailed description:

The Validation control is an applicative functor and facilitates accumulating errors. When trying to compose Monads, the combination process will short circuit at the first encountered error. But Validation will continue processing the combining functions, accumulating all errors. This is especially useful when doing validation of multiple fields, say a web form, and you want to know all errors encountered, instead of one at a time.

If you are not used to functional slang, this description may sound a little bit overwhelming at first. Let’s clarify it, in simple English this means: one can validate many properties, such as names, zip codes and lengths, without stopping at the first error. Even cooler is that validations, which do not depend on each other, can be done in parallel. Try that with exceptions! A good overview on validations you can find in Cats, the functional programming library for Scala.

Side note

Validations are derived from Either, which can be used for exception handling, too. The advantage of validations is that they provide richer functionality for error handling.

Back to validations: they give you the tools to continue with the »flow«, which otherwise would break with exceptions. So what is meant with breaking the flow?

public static Address validate(String street, String zipCode) {

    if (!validStreet(street)) {
        throw new IllegalArgumentException("street is not valid");
    } else if (!validZipCode(zipCode)) {
        throw new IllegalArgumentException("zipcode not valid");
    }  // ...

    return new Address(street, zipCode);
}

Calling validate requires a try/catch block with proper exception handling and calling it with e.g. an invalid zip code and place will give you only an exception about the invalid zip code, but not about place. This “error - fix - next error - next fix - …” pattern is not very handy for e.g. user forms. To implement exception handling in vanilla Java consistently can be a challenging task.

My mama always said, ‘Life was like a box of chocolates. You never know what you’re gonna get.’

Forrest Gump

Validations take away this pain and stick to the principle of least surprise: they keep the contract a function has with its caller. You might reply: “Yeeesss, but in Java I expect exceptions everywhere, so I’m not surprised anymore”. An example might clarify the advantage:

Seq<String> content;
try {
   content = jsonDocuments
      .map(Common::parseJson)
      .map(node -> node.get("url").asText())
      .map(Common::loadContent);
} catch (Exception ex) {
    // ...
}

If only one function call in the example above throws an exception, the complete flow breaks. One has to deal with potential exceptions in parseJson and loadContent, requiring additional code. The same example with validations:

List<Validation<List<DomainError>, String>> validatedContent = jsonDocuments
   .map(Common::parseJsonValidated)
   .map(v -> v.map(node -> node.get("url").asText()))
   .map(v -> v.flatMap(Common::loadContentValidated))
   .filter(Validation::isValid);

Seq<String> content = Validation.sequence(validatedContent).get();

Here, the flow is resilient and the results can be extracted easily. Errors are handled analogously (not shown).

In contrast to exceptions, where the exception type needs to be inspected by the caller, validations encapsulate errors and their types. Additional inspection can be skipped. This fact becomes clearer with an example function. Let’s validate a country name and return its CountryCode:

public static CountryCode validateCountry(String country) {
     for(CountryCode cc : CountryCode.values()) {
         if(cc.name().toLowerCase().equals(country.toLowerCase())) {
             return cc;
         }
     }
     throw new RuntimeException("could not find country code");
}

From the signature of the function, it is not possible to infer what happens if the validation does not succeed. In plain Java, you never know what you’re gonna get: you can hardly predict if an and what type of (Runtime-) exception will pop up during the function call.

Implementation

Still, there are not only benefits to validations: they require some initial effort, because you actually have to deal with errors on the spot and you cannot mute them away with catch-blocks. However, a systematic approach will speed you up in the long term. How does the code look like? Let’s assume we are the backend of a webapp and we need to validate address information for a web form:

public static Validation<Seq<DomainError>, Address>
validate(String street,
         String zipCode) {

   return Validation.combine(
           validateStreet(street),
           validateZipCode(zipCode)
   ).ap(Address::new);
}

A validation function takes input values and returns the correctly typed input, which is in this case an Address. The validate function is comprised of separate functions. Each function is validating a part of the address and returns a validation object. In the validate method all validations are combined into an Address object, or, if there are errors, a list of DomainError-objects instead. A validation function simply might check the maximum length of an input field:

private static final int MAX_STREET_LENGTH = 500;
private static Validation<DomainError, String> validateStreet(String street) {
      return street.length() > MAX_STREET_LENGTH ?
              Validation.invalid(new AddressError(
                      "street",
                      String.format("Street name must not be longer than %s", MAX_STREET_LENGTH))) :
              Validation.valid(street);
  }

Or it is possible to wrap validation functions from libraries or existing code in validations:

private static Validation<DomainError, CountryCode> validateCountry(String country) {
    return Try.of(() -> findCountryCode(country))
            .map(Validation::<DomainError, CountryCode>valid)
            .getOrElseGet(ex -> Validation.invalid(new DomainError(ex.getMessage())));
            // or alternatively (if the message can be neglected)
            //.getOrElse(Validation.invalid(new DomainError("Unknown country")));
}

When using validation control it is often beneficial to introduce a general DomainError class. In its most simplest form it could look like:

import lombok.Getter;

@Getter
public class DomainError {
    protected String message;

    public DomainError(String message) {
        this.message = message;
    }
    public RuntimeException toException() {
        return new RuntimeException(this.message);
    }
}

If validations are used in big systems, DomainError can serve as parent for more specific errors, such as e.g. InvalidAddress or UserNotFound.

At the very end, when the result or error message is sent to a service or user, validations are unpacked (or in functional slang: folded).

public Result getUserMail(String userId) {
    return userService
        .getUser(userId)
        .map(User::getEmail)
        .fold(
            error -> status(Http.Status.BAD_REQUEST, error.toString()),
            email -> status(Http.Status.OK, email)
    );
}

Of course and if possible, validations can be resolved already earlier.

Protect your context boundary

This approach fits well into the concepts of Domain-driven Design:

validations & bounded context

Within the context (or microservice) boundary, validations with domain errors e.g. Validation<InvalidCredentials, User> are used. Only when leaving the context boundary, validation errors should be transformed into appropriate errors, e.g. a HTTP 403 - forbidden response. This is a very powerful approach, because implementation details in the adapter layer (see figure) do not leak into your bounded context. You could switch the framework you use in the RESTful adapter from Spring to Play without touching the code inside the bounded context, i.e. your pure domain code.

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 advantages. Note that this is not a black-white topic, so it is absolutely fine to have exceptions and validations living next to each other. Best example is Lagom, where exceptions are returned by PersistentEntity-classes. In this case it may be better to stick to the approach suggested by the framework or take a hybrid approach.

Have fun!

(In case you did not see it yet, there is also a second part)