Error Handling With Try and Either
When programming in Java you can no longer run away from handling errors and exceptions in your code. Throwing and catching exceptions is a “glorified goto” and also hidden performance costs (why is creating a Throwable
so expensive? It’s because usally it will call Throwable.fillInStackTrace()
).
Let’s first have a look at this approach:
public class Customer {
private final int age;
public int getAge() {
return age;
}
}
public class Alcoholic {
private final int alcoholConcentration;
public int getAlcoholConcentration() {
return alcoholConcentration;
}
}
public class UnderageException extends Exception {
public UnderageException(String message) {
super(message);
}
}
Alcoholic buyAlcoholic(Customer customer) throws UnderageException {
if (customer.getAge() < 18) {
throw new UnderageException("Age: " + customer.getAge());
}
return new Alcoholic(5);
}
try {
Customer tooYoung = new Customer(16);
Alcoholic alcoholic = buyAlcoholic(tooYoung);
} catch (UnderageException ex) {
// Troubles
}
Nasty, isn’t it? And doesn’t really go well with functional programming (or other situation like concurrency).
The functional way
In Scala, there is a specific type that represents computations that may result in an exception: Try
(there is also a similar type, called Either
, but we’ll cover it later), is there a way to get it in Java also?
VAVR to the rescue!
Try is a monadic container type which represents a computation that may either result in an exception, or return a successfully computed value. Instances of
Try
, are either an instance ofSuccess
orFailure
.
An instance of Success<A>
simply wraps a value of type A
, an instance of Failure<A>
wraps a Throwable (i.e. an exception or other kind of error).
Try<Alcoholic> alcoholic = Try.of(() -> buyAlcoholic(new Customer(16)));
This beauty returns a value of type Try<Alcoholic>
. If the given Customer has a legal age, this will be a Success<Alcoholic>
, otherwise the buyAlcoholic
method throws a UnderageException, however, it will be a Failure<Alcoholic>
.
Working with Try instances is actually very similar to working with Option values: you can check if a Try is a success by calling isSuccess
and then retrieve the wrapped value with get
, but there aren’t many situations where you will want to do that.
Normally you want to return a default value (using orElse*
or getOrElse*
methods) or transform a failure into a success (using recover*
or recoverWith*
methods).
Chain operations
Like Option, Try supports all the higher-order methods you know from other monadic containers:
Try<Integer> concentration = alcoholic.map(Alcoholic::getAlcoholConcentration);
Mapping a Try<Alcoholic>
that is a Success<Alcoholic>
to a Try<Integer>
results in a Success<Integer>
. If it’s a Failure<Alcoholic>
, the resulting Try<Integer>
will be a Failure<Integer>
, on the other hand, containing the same exception as the Failure<Alcoholic>
.
Filter and foreach
It is also passible to filter a Try or call foreach on it.
The filter method returns a Failure
if the Try on which it is called is already a Failure
or if the predicate passed to it returns false (in which case the wrapped exception is a NoSuchElementException
).
The function passed to foreach is executed only if the Try is a Success
, which allows you to execute a side-effect.
Pattern Matching
If you want to know whether a Try
instance you have received as the result of some computation represents a success or not and execute different code branches depending on the result you make use of pattern matching:
String result = Match(concentration).of(
Case($Success($()), c -> "drinked " + c),
Case($Failure($()), t -> "troubles"));
Recovering
If recover is called on a Success
instance, that instance is returned as is. Otherwise, if the partial function is defined for the given Failure
instance, its result is returned as a Success
.
Try<Integer> recovered = concentration
.recover(th -> Match(th).of(
Case($(instanceOf(UnderageException.class)), () -> 0),
Case($(), () -> -1)));
Try conclusion
The Try
type allows to encapsulate computations that result in errors in a container and to chain operations on the computed values in a very elegant way.
In the next part we are going to deal with Either
, an alternative type for representing computations that may result in errors, but with a wider scope of application that goes beyond error handling.
Comments