A Java result library helping you get rid of exceptions, enabling a more fluent coding style.
Programming with exceptions can be both tedious and error-prone. Checked exceptions gives much boilerplate code, while unchecked exceptions can be the source of errors if they are let loose in the system without sufficient handling.
Also exceptions should not be used for control-flow, but in the heat of the moment it is often faster to just throw an IllegalArgumentException than encapsulating the error in the returned class. This result library has done the encapsulation of the error for you, making your control flow more fluent and exception-free.
The introduction of Optional was a great step forward for Java, but as a class it does not make it any easier to return and handle error situations. The Result classes from this library is inspired from the Optional-class providing much of the same functionality, enabling utilization of chaining and lambdas for a more fluent style of programming.
Make methods that can give an error return a Result<T, E>
where T is the type of the return value when the result is in success state, and E is the type of error value when the result is in error state.
public Result<Customer, String> getCustomer(String id) {
try {
Customer customer = service.getCustomer(id);
return Result.success(customer);
} catch (RuntimeException ex) {
return Result.error(ex.getMessage());
}
}
Now the code can be used like this:
Result<Customer, String> customerResult = getCustomer(id);
You may also use the static factory method handle
which runs a given Callable. If it runs successfully the return value of the Callable will be the success value of the Result. If an exception is thrown it will be the error value of the Result. Callable is used as parameter as it supports checked exceptions.
Result<Customer, Exception> customerResult = Result.handle(
() -> service.getCustomer(id));
An exception mapper may also be provided to the handle
method for mapping the exception if thrown.
Result<Customer, String> customerResult = Result.handle(
() -> service.getCustomer(id),
Exception::getMessage);
However the full benefit of using the Fluent Result library is more visible when using chaining and utilizing other methods that return Result values.
Some other methods to use while chaining:
public Result<Account, String> findAccount(List<Account> accounts, String accountId) {...}
public void logAccountInfo(Account account) {...}
public void logError(String customerId, String accountId, String errorMsg) {...}
Now the above methods can be called by chaining the calls.
public Result<BigDecimal, String> getBalance(String customerId, String accountId) {
return Result.handle(() -> service.getCustomer(id))
.mapError(Exception::getMessage)
.map(Customer::getAccounts)
.flatMap(accounts -> findAccount(accounts, accountId))
.consume(this::logAccountInfo)
.map(Account::getBalance)
.consumeError(errMsg -> logError(customerId, accountId, errMsg));
}
In the above chain the methods map
, flatMap
and consume
are invoked only if the Result is in success state and contains a success value, and not if an error were returned somewhere in the chain, giving a Result in error state. The methods mapError
and consumeError
is only invoked if error state.
Some cases of Result value types frequently happen, like Result<Optional<T>, E>
, Result<Boolean, E>
and Result<Void, E>
. For these frequent result types the additional Result classes OptionalResult, BooleanResult and VoidResult are provided.
These classes provide some additional methods relevant for their type, and remove methods not relevant for their type. They create a better facade for some of the recurring patterns happening when using the regular Result.
Comparing using Result<Optional<T>, E>
and OptionalResult<T, E>
, first with Result<Optional<Customer>, E>
:
public Result<Optional<Integer>, String> getAge(String customerId) {
return getCustomer(customerId) // Returns Result<Optional<Customer>, String>
.consume(maybeCustomer -> maybeCustomer.ifPresentOrElse(
customer -> LOGGER.info("Customer + " customer.getName() + " found"),
() -> LOGGER.warn("Customer not found")
))
.map(maybeCustomer -> maybeCustomer.map(Customer::getAge));
}
By using OptionalResult<Customer, E>
the chain is more readable when methods like consumeValue
, runIfEmpty
and mapValue
may be used:
public OptionalResult<Integer, String> getAge(String customerId) {
return getCustomer(customerId) // Returns OptionalResult<Customer, String>
.consumeValue(customer -> LOGGER.info("Customer + " customer.getName() + " found"))
.runIfEmpty(() -> LOGGER.warn("Customer not found"))
.mapValue(Customer::getAge);
}
Comparing using Result<Boolean, E>
and BooleanResult<E>
, first with Result<Boolean, E>
:
public boolean isOldEnough(String customerId) {
return getCustomer(customerId) // Returns Result<Customer, String>
.map(customer -> customer.getAge() >= 18) // Returns Result<Boolean, String>
.runIfSuccess(oldEnough -> {
if (oldEnough) {
LOGGER.info("Customer is old enough");
} else {
LOGGER.warn("Customer is underage");
}
})
.runIfError(errMsg -> LOGGER.warn("Error getting customer: " + errMsg))
.orElse(false);
}
By using BooleanResult<E>
the chain is more readable when methods like runIfTrue
, runIfFalse
and orElseFalse
may be used:
public boolean isOldEnough(String customerId) {
return getCustomer(customerId) // Returns Result<Customer, String>
.mapToBoolean(customer -> customer.getAge() >= 18) // Returns BooleanResult<String>
.runIfTrue(() -> LOGGER.info("Customer is old enough"))
.runIfFalse(() -> LOGGER.warn("Customer is underage"))
.runIfError(() -> LOGGER.warn("Error getting customer"))
.orElseFalse();
}
OptionalResult and BooleanResult also have three argument versions of the consumeEither
, runEither
and fold
methods.
The above example, modified to runEither
:
public boolean isOldEnough(String customerId) {
return getCustomer(customerId) // Returns Result<Customer, String>
.mapToBoolean(customer -> customer.getAge() >= 18) // Returns BooleanResult<String>
.runEither(
() -> LOGGER.info("Customer is old enough"),
() -> LOGGER.warn("Customer is underage"),
() -> LOGGER.warn("Error getting customer"))
.orElseFalse();
}
The additional Result classes also have static factory methods relevant for their value type, examples include OptionalResult.successNullable(T value)
, OptionalResult.empty()
, BooleanResult.successTrue()
and VoidResult.success()
.
Creating an OptionalResult:
public OptionalResult<Customer, String> getCustomer(String id) {
try {
Customer customer = service.getCustomer(id); // May be null
return OptionalResult.successNullable(customer);
} catch (RuntimeException ex) {
return OptionalResult.error(ex.getMessage());
}
}
All Result classes have methods for mapping and flat mapping to the other Result classes. Example of mapping from a Result to a BooleanResult by using the method mapToBoolean
:
public BooleanResult<String> isOldEnough(String customerId) {
return getCustomer(customerId)
.mapToBoolean(customer -> customer.getAge() >= 18) // Returns BooleanResult<String>
.runIfFalse(() -> LOGGER.warn("Customer is underage"));
}
The method verify
can be used for verifying the value of the Result, and if the verification fails the returned Result will contain an error value.
There are two methods for verifying the result. The first method takes a single function mapping the success value to a VoidResult. The other method takes two arguments, a predicate taking in the success value, and an error supplier providing the value if the predicate evaluates to false.
Example of verifying the age of a customer by using a method which returns VoidResult. If the customer is under 18 then the returned Result will contain the error message from the VoidResult.
public Result<Customer, String> getGrownUpCustomer(String customerId) {
return getCustomer(customerId) // Returns Result<Customer, String>
.verify(this::isCustomerGrownUp);
}
private VoidResult<String> isCustomerGrownUp(Customer customer) {
if (customer.getAge() >= 18) {
return VoidResult.success();
}
return VoidResult.error("Customer is not a grown up");
}
Sometimes you want to verify something without the overhead of creating a method returning a VoidResult, then you may use a predicate and error supplier instead.
Example of verifying the age of a customer. If the customer is under 18 then the returned Result will contain the error message provided by the error supplier.
public Result<Customer, String> getGrownUpCustomer(String customerId) {
return getCustomer(customerId) // Returns Result<Customer, String>
.verify(
customer -> customer.getAge() >= 18,
() -> "Customer is not a grown up");
}
For OptionalResult you may also verify the actual value if it is present. If the OptionalResult was already empty it will remain empty. Verifying the value also supports both providing a function mapping to VoidResult, or using a predicate and error supplier.
public Result<Customer, String> getGrownUpCustomer(String customerId) {
return getCustomer(customerId) // Returns OptionalResult<Customer, String>
.verifyValue(
customer -> customer.getAge() >= 18,
() -> "Customer is not a grown up");
}
The Result classes does not have methods like get
, isPresent
or isSuccess
. We have seen these kind of methods misused on Optional, being the source of errors, or they may be used as an easy way out when the programmer don't want to program in a functional style.
Example of code we do not want:
Result<Customer, String> customerResult = getCustomer(id);
if (customerResult.isSuccess()) {
doSomethingWithCustomer(customerResult.get());
} else {
logAnError(customerResult.getError());
}
Instead it should be implemented like this:
getCustomer(id).consumeEither(
this::doSomethingWithCustomer,
this::logAnError);
If you want to extract the value from the Result-object without consuming, the methods orElse
, orElseGet
and orElseThrow
known from Optional may be used. Also provided are a method fold
which can be used for mapping both the success value and the error value to a single return value.
Example of fold
:
public Status getCustomerStatus() {
return getCustomer(id) // Returns Result<Customer, String>
.fold(
customer -> Status.CUSTOMER_EXISTS,
error -> Status.CUSTOMER_FETCH_ERROR);
}
There are also three argument versions of the fold
method on OptionalResult and BooleanResult. Example with OptionalResult:
public Status getCustomerStatus() {
return getCustomer(id) // Returns OptionalResult<Customer, String>
.fold(
customer -> Status.CUSTOMER_EXISTS,
() -> Status.CUSTOMER_NOT_FOUND,
error -> Status.CUSTOMER_FETCH_ERROR);
}
Example with BooleanResult:
public Status getCustomerStatus() {
return getCustomer(id) // Returns Result<Customer, String>
.mapToBoolean(customer -> customer.getAge() >= 18) // Returns Booleanesult<String>
.fold(
() -> Status.CUSTOMER_APPROVED,
() -> Status.CUSTOMER_UNDERAGE,
error -> Status.CUSTOMER_FETCH_ERROR);
}
success(T value)
error(E value)
handle(Callable<T> callable)
handle(Callable<T> callable, Function<Exception, E> exceptionMapper)
)
map(Function<T, N> function)
mapToOptional(Function<T, Optional<N>> function)
mapToBoolean(Function<T, Boolean> function)
mapError(Function<E, N> function)
flatMap(Function<T, Result<N, E>> function)
flatMapToOptionalResult(Function<T, OptionalResult<N, E>> function)
flatMapToBooleanResult(Function<T, BooleanResult<E>> function)
flatMapToVoidResult(Function<T, VoidResult<E>> function)
consume(Consumer<T> consumer)
consumeError(Consumer<E> consumer)
consumeEither(
Consumer<T> valueConsumer,
Consumer<E> errorConsumer)
flatConsume(Function<T, VoidResult<E> function)
runIfSuccess(Runnable runnable)
runIfError(Runnable runnable)
runEither(
Runnable successRunnable,
Runnable errorRunnable)
runAlways(Runnable runnable)
flatRunIfSuccess(Supplier<VoidResult<E> supplier)
verify(Function<T, VoidResult<E>> function)
verify(
Predicate<T> predicate,
Supplier<E> supplier)
fold(
Function<T, N> valueFunction,
Function<E, N> errorFunction)
orElse(T other)
orElseGet(Function<E, T> function)
orElseThrow(Function<E, X> function)
toOptionalResult()
toVoidResult()
success(Optional<T> maybeValue)
success(T value)
successNullable(T value)
empty()
error(E value)
handle(Callable<T> callable)
handle(Callable<T> callable, Function<Exception, E> exceptionMapper)
)
map(Function<Optional<T>, N> function)
mapToOptional(Function<Optional<T>, Optional<N>> function)
mapToBoolean(Function<Optional<T>, Boolean> function)
mapError(Function<E, N> function)
mapValue(Function<T, N> function)
mapValueToOptional(Function<T, Optional<N>> function)
flatMap(Function<Optional<T>, Result<N, E>> function)
flatMapToOptionalResult(Function<Optional<T>, OptionalResult<N, E>> function)
flatMapToBooleanResult(Function<Optional<T>, BooleanResult<E>> function)
flatMapToVoidResult(Function<Optional<T>, VoidResult<E>> function)
flatMapValueWithResult(Function<T, Result<E>> function)
flatMapValueWithOptionalResult(Function<T, OptionalResult<E>> function)
flatMapValueWithBooleanResult(Function<T, BooleanResult<E>> function)
consume(Consumer<Optional<T>> consumer)
consumeValue(Consumer<T> consumer)
consumeError(Consumer<E> consumer)
consumeEither(
Consumer<Optional<T>> successConsumer,
Consumer<E> errorConsumer)
consumeEither(
Consumer<T> valueConsumer,
Runnable emptyRunnable,
Consumer<E> errorConsumer)
flatConsume(Function<Optional<T>, VoidResult<E> function)
flatConsumeValue(Function<T> function)
runIfSuccess(Runnable runnable)
runIfValue(Runnable runnable)
runIfNoValue(Runnable runnable)
runIfEmpty(Runnable runnable)
runIfError(Runnable runnable)
runEither(
Runnable successRunnable,
Runnable errorRunnable)
runEither(
Runnable valueRunnable,
Runnable emptyRunnable,
Runnable errorRunnable)
runAlways(Runnable runnable)
flatRunIfSuccess(Supplier<VoidResult<E> supplier)
flatRunIfValue(Supplier<VoidResult<E> supplier)
verify(Function<Optional<T>, VoidResult<E>> function)
verify(
Predicate<Optional<T>> predicate,
Supplier<E> supplier)
verifyValue(Function<T, VoidResult<E>> function)
verifyValue(
Predicate<T> predicate,
Supplier<E> supplier)
fold(
Function<Optional<T>, N> successFunction,
Function<E, N> errorFunction)
fold(
Function<T, N> valueFunction,
Supplier<N> emptySupplier,
Function<E, N> errorFunction)
orElse(Optional<T> other)
valueOrElse(T other)
orElseGet(Function<E, Optional<T>> function)
valueOrElseGet(Supplier<T> function)
orElseThrow(Function<E, X> function)
valueOrElseThrow(Supplier<X> function)
toResult(Supplier<E> errorSupplier)
toVoidResult()
success(boolean value)
successTrue()
successFalse()
error(E value)
handle(Callable<T> callable)
handle(Callable<T> callable, Function<Exception, E> exceptionMapper)
)
map(Function<Boolean, N> function)
mapToOptional(Function<Boolean, Optional<N>> function)
mapToBoolean(Function<Boolean, Boolean> function)
mapError(Function<E, N> function)
flatMap(Function<Boolean, Result<N, E>> function)
flatMapToOptionalResult(Function<Boolean, OptionalResult<N, E>> function)
flatMapToBooleanResult(Function<Boolean, BooleanResult<E>> function)
flatMapToVoidResult(Function<Boolean, VoidResult<E>> function)
consume(Consumer<Boolean> consumer)
consumeError(Consumer<E> consumer)
consumeEither(
Consumer<Boolean> successConsumer,
Consumer<E> errorConsumer)
consumeEither(
Runnable trueRunnable,
Runnable falseRunnable,
Consumer<E> errorConsumer)
flatConsume(Function<Boolean, VoidResult<E>> function)
runIfSuccess(Runnable runnable)
runIfTrue(Runnable runnable)
runIfFalse(Runnable runnable)
runIfError(Runnable runnable)
runEither(
Runnable successRunnable,
Runnable errorRunnable)
runEither(
Runnable trueRunnable,
Runnable falseRunnable,
Runnable errorRunnable)
runAlways(Runnable runnable)
flatRunIfSuccess(Supplier<VoidResult<E>> supplier)
verify(Function<Boolean, VoidResult<E>> function)
verify(
Predicate<Boolean> predicate,
Supplier<E> supplier)
fold(
Function<Boolean, N> successFunction,
Function<E, N> errorFunction)
fold(
Supplier<N> trueSupplier,
Supplier<N> falseSupplier,
Function<E, N> errorFunction)
orElse(Boolean other)
orElseTrue()
orElseFalse()
orElseGet(Function<E, Boolean> function)
orElseThrow(Function<E, X> function)
toOptionalResult()
toVoidResult()
handle(CheckedRunnable runnable)
handle(CheckedRunnable runnable, Function<Exception, E> exceptionMapper)
mapError(Function<E, N> function)
replace(Supplier<N> supplier)
replaceWithOptional(Supplier<Optional<N>> supplier)
replaceWithBoolean(Supplier<Boolean> supplier)
flatReplace(Supplier<Result<N, E>> supplier)
flatReplaceToOptionalResult(Supplier<OptionalResult<N, E>> supplier)
flatReplaceToBooleanResult(Supplier<BooleanResult<E>> supplier)
flatReplaceToVoidResult(Supplier<VoidResult<E>> supplier)
consumeError(Consumer<E> consumer)
consumeEither(
Runnable successRunnable,
Consumer<E> errorConsumer)
runIfSuccess(Runnable runnable)
runIfError(Runnable runnable)
runEither(
Runnable successRunnable,
Runnable errorRunnable)
runAlways(Runnable runnable)
flatRunIfSuccess(Suppluer<VoidResult<E>> supplier)
fold(
Supplier<N> valueSupplier,
Function<E, N> errorFunction)
orElseThrow(Function<E, X> function)