Error handling in java applications

February 4, 2020
error handling in java applications

This article explores a more functional way of handling errors in java applications. Throughout the code samples, I will be using some constructs provided by the excellent library vavr.

In the examples I will also be using lombok and jooq. I will not go into details regarding these libraries, instead just use them to get the point across.

The domain I am going to model is very simple: the system has items and users.

The language permits different mechanisms for error handling:

  • Exceptions They come in two flavors — checked and unchecked. The first type is enforced at runtime. Using checked exceptions as a way of signaling possible failures allows for a sort of documentation of the function directly in its signature, but it does lead to verbose try/catch blocks. Unchecked exceptions alleviate the mandatory try/catch blocks, but the function signature loses its expressiveness.

  • Another mechanism of handling exceptions is lifting the computation result in an Optional value. While this does signal if the computation was successful or not, it does not carry the failure information.

  • The Try construct helps with preserving the error information, by dedicating a channel to the Throwable instance. The downside here is that Throwable is a broad type, and it does not communicate the reasons for the failure.

  • This brings us to a data type that dedicates an error channel and also has a return type — Either[Left, Right].


Understanding Either

The Either type represents values with two possibilities: a value of type Either a b is either Left a or Right b. In the context of error handling the left type is used for the error and the right type for the computation result.

Either is also a monad. You can view a monad as a wrapper. The advantage is that the wrapper will provide functions, unrelated to the wrapped object, that make composition a breeze. Specifically the functions are map and flatMap.

Through map you can apply a transformation on the wrapped object. A simple square function could look like:

either.map(rightIntValue -> rightIntValue * rightIntValue)

Note that the map function doesn't need to return the same type as the input.

flatMap is a function that expects a function that returns a collection of elements. Like map, it then applies that function to all elements of the collection it has been called from. But unlike map, it takes one more step and flattens the resulting groups of collections into the original collection. A very powerful trait of flatMap can be observed when dealing with other nested monads. Using Either as an example:

Either<Error, SomeResult> someFn();
Either<Error, OtherResult> otherFn(SomeResult res);

//evaluates to Either<Error, Either<OtherResul>>
someFn().map(someResult -> otherFn(someResult));

//evaluates to Either<Error, OtherResult>
someFn().flatMap(someResult -> otherFn(someResult));

Either is right biased, meaning that the map and flatMap operations will be applied on the right projection by default. As a consequence, if at some point in the chain a Left is returned, then the whole chain will be short-circuited, yielding the left projection.

Repository > Service > Facade > Controller

With that in mind, let’s see what the repository layer might look like:

Either<DbError, List<Item>> fetchItems();
Either<DbError, Option<Item>> fetchItem(ItemId itemId);

Notice here the usage of the Error suffix, rather than the Exception. This is used to highlight to the reader the divergence from the common exception model. So the ItemsDbError can be defined:

@RequiredArgsConstructor
public final class ItemsDbError {
   public final String reason; 
}

Also, the function signature is expressive, describing to the reader what to expect when calling it. What this implies is that ideally, the function will not allow any exceptions to be raised. To this end vavr’s Try construct helps by lifting a block of code into its semantics and projecting to an Either. With this in mind a generic db call wrapper might look something like:

protected <T> Either<ItemsDbError, T> tryDbCall(Supplier<T> dbCall) {
return Try.of(dbCall::get)
.fold(
dbError -> {
logger.warn("Problem executing db operation", dbError);
return Left(new ItemsDbError(dbError.getMessage()));
},
Either::right
);
}

The fold operator is used to converge the two Try channels, success and failure, to a single type, which in this case is an Either. Again, left projection for error channel, the right one for the operation result value.

Going up a level

Repository functions are typically used as data sources for the service layer.

Either<?, MostRelevantItem> serviceLayerFunction() {
   return itemsRepo.fetchItems()
        .flatMap(this::theMostRelevantItem)
} 
Either<ItemProcessingError, MostRelevantItem> theMostRelevantItem(List<Item> candidates) { ... } 

Question is, what type should the ? have. The idea is to have the function signature be as helpful as it can be for a developer. This means relevant return types. To that end, leaving the left channel as an ItemDbError wouldn't make sense. At the same time, ItemProcessingError doesn't fully describe the possible errors either. Languages with a more advanced type system, have baked in the concept of union types allowing declarations similar to ItemDbError Or ItemProcessingError.

In order to bypass this type system limitation, you can model the error channel based on the domain at hand. In this case both of exceptions belong to an Items domain, which opens the possibility of an interface abstraction:

interface ItemsError {} 
public static class ItemsDbError implements ItemsError
public static class ItemsProcessingError implements ItemsError
Either<ItemsError, MostRelevantItem> serviceLayerFunction() {

This approach allows for modeling the error domain, and, at the same time, allow the type system to act as a sort of documentation for discovering what error type might be encountered when calling the function. Keeping the error declarations in the same file makes sense, instead of having them scattered all over the code repository.

What about different domains?

As an example let’s consider an items function that requires user information. The user information is obtained from the web request context. It is preferable to keep the ItemsService unaware of the HTTP request context; it needs to read user information, how that user information came to be is of no interest to it.

Either<ItemsError, SomeResult> userItemsFn(UserId userId) { ... }

Extracting user information:

public Option<UserId> userId() {
    return Option.of(accessToken())
        .map(JsonWebToken::getSubject)
        .map(subject -> new UserId(UUID.fromString(subject)));
}

and using it in the facade:

Either<UserNotPresentError, UserId> userOrFail(RequestContext rc) {
    return rc.userId()
             .toEither(UserNotPresentError::new);
}
? itemsFn(){
   return userOrFail.map(userId -> itemsService.userItemsFn(userId))
}

UserNotPresentError belongs to a users domain and has no knowledge whatsoever of the items domain. So how can you bypass the type system limitations mentioned before? One way is to consider them as part of a more generic error domain — MyAppError. Another one is to stack Eithers, which obviously isn’t really scalable, as it would make the return type of the function impossible to read. Personally I think that this is not a problem when there are only two domains interacting, as it translates to:

Either<UserNotPresentError, Either<ItemsError, SomeResult>> itemsFn

Not pretty, but it conveys the message.

Last stop before sending back the response a.k.a controller layer

Normally, exceptions can be handled in the controller level, and more elegantly in custom exception mappers. In a sense this follows the same narrative of reasoning about exceptions in a separate channel rather than the unified perspective.

In the approach described by this article it is required to converge both channels to a HTTP response; Spring’s ResponseEntity, or jaxrs’s Response, whatever framework is used. The fold operation is ideal for such a thing, allowing for:

protected final Function<UserNotPresentError, ResponseEntity<?>> userFacadeErrorHandler =
userNotPresentError -> ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
protected final Function<Either<ItemsError, ?>, ResponseEntity> resultHandler = serviceResult -> serviceResult.fold(
itemsError -> handleItemsError(itemsError),
successValue -> Match(successValue).of(
Case($(), () -> ResponseEntity.ok(successValue).build())
)
);
protected ResponseEntity handleItemsError(ItemsError itemsError) {
return Match(itemsError).of(
Case($(instanceOf(DbError.class)), (e) -> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.reason)),
Case($(instanceOf(ItemsProcessingError.class)), (e) -> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.reason)),
);
}
@GetMapping("/some-items-endpoing")
ResponseEntity controllerMethod() {
authenticatedItemsFacade.itemsFn().fold(userFacadeErrorHandler, resultHandler);
}

Conclusion

This article explored a different approach of handling errors applied in the context of a web application. Choosing this way of thinking about managing errors in a program bypasses the exception model that the language offers. In return, we get to reason about successful execution and errors in a unified manner and implicitly get expressive function signatures.

One thing to take into consideration is that most of the heavy lifting is done by the vavr library. Unfortunately, Java doesn’t yet offer these constructs. More importantly, if thinking of trying this approach in a project, have the team onboard first. It will be hard to read code in a larger application that uses exceptions as well. At the end of the day productivity is the most important factor in a project.