DEV Community

Sergiy Yevtushenko
Sergiy Yevtushenko

Posted on • Edited on

Consistent error propagation and handling in Java

Every application lives in real world and real world is not perfect. So even ideal, bug-free application doomed to deal with errors.

This problem exists since very birth of first computer program. And software engineers invented many ways to deal with errors.

Java traditionally uses following approaches to signal caller that there is an error:

  • return special value (most often 'null' value is used for this purpose)
  • throw an exception

Both these approaches have significant drawbacks.

Returning special value discards information about actual cause of the error and bloats the code with additional checks.

Exceptions are quite expensive comparing to normal execution flow and are making flow hard to follow and hard to verify for correctness. Some libraries and frameworks tend to abuse exceptions up to making them part of normal execution flow, which is insane.

So, is there any alternative way to inform caller about errors without mentioned above drawbacks? Yes! Functional programming provides one.

Note that in following text I'll try to avoid FP-specific terminology. This does not make approach less functional, but simplifies understanding of the concept for those who is not yet get used to FP-slang.

The Either<L, R> container

The idea is to use container for return value instead of plain value. The container is special: while being declared for two types, it actually holds only one value at a time of either first or second type.

The Either<L, R> is general purpose container, not tied to error propagation/handling. But when it is used for this particular purpose, then, by convention, first (or "left") type is used to represent error type, while second (or "right") type represents return value type.

In code this looks like this:


   Either<ErrorDetails, UUID> parseUUID(final String input) {
       ...
       // failure
       return Either.left(ErrorDetails.of("Unable to parse UUID"));
       ...
       // success 
       return Either.right(uuid);
   }

Enter fullscreen mode Exit fullscreen mode

In fact, there is not so much difference from usual "do something and return result if success or throw an exception if there is an error".

But deeper look exposes a lot of advantages:

  • No need anymore to return some "special" value.
  • Information about error is still available.
  • Execution flow is not broken.

Code above shows "producing" side, now let's take a look how "consuming" side looks like:

   ...// Service interface
   Either<ErrorDetails, User> getUserById(final UUID uuid);

   ...//Actual use
   return parseUUID(parameter).flatMapRight(service::getUserById); 

Enter fullscreen mode Exit fullscreen mode

This suspiciously simple code contains everything necessary to handle errors:

  • It returns correct error result if any processing step returns error.
  • It stops processing immediately once error occurred.
  • It does not break execution flow, return statement is always executed and always returns value to caller.
  • It enforces "either handle error or propagate it" policy, which results to robust code.
  • Consistent application of this approach results to clean and readable code.

Specializing to Narrow Use Case

As one might notice, plain Either<L, R> is quite verbose when used for error handling.

First of all, it requires error type to be explicitly referenced, although usually there is not so many base types for errors. For example, Java uses single Throwable type as base class for all errors and exceptions.

Second source of verbosity and inconvenience (for this particular purpose) is that Either<L, R> is general in the sense that it can be used for any types and its API is symmetric in regard to both sides. When Either<L, R> is used for error handling, this requires consistent application of some convention, like mentioned above.

So, for narrower case of error handling, Either<L, R> can be specialized into Result<T> type, which assumes single common base type for errors and has API tuned for error handling. This makes code less verbose and less prone to accidental mistakes.

With Result<T> code above can be rewritten to following:


   ...

   Result<UUID> parseUUID(final String input) {
       ...
       return Result.failure(ErrorDetails.of("Unable to parse UUID"));
       ...
       return Result.success(uuid);
   }

   ...// Service interface
   Result<User> getUserById(final UUID uuid);

   ...
   return parseUUID(parameter).flatMap(service::getUserById); 

Enter fullscreen mode Exit fullscreen mode

Now code is less verbose while all mentioned above properties still present.

Adapting Existing Code to use Result<T>

Use of Result<T> is convenient in your own code, but we're living in the world of Java libraries and frameworks which don't use it. They throw exceptions and return null's. So, we need convenient way to interact with existing code.

For this purpose Result<T> implementation in Reactive Toolbox Core provides set of helper methods which allow to wrap traditional methods into ones returning Result.

Example below shows how these helper methods can be used:

   interface PageFormattingService {
       Result<Page> format(final URI location);
   }

   private PageFormattingService service;

   private Result<Page> formatPage(final String requestUri) {
       return lift(URI::create)
               .apply(requestUri)
               .flatMap(service::format);
   } 
Enter fullscreen mode Exit fullscreen mode

Afterword

This article (along with previous one ) is an attempt to describe some main concepts of Reactive Toolbox Core library. Of course, none of these concepts are new. I'm just trying to create library which enables convenient and consistent application of these concepts.

I often see whole articles dedicated to "Java is too old and should be retired and replaced with modern language". The concepts mentioned above show that this is simply not true. Within existing Java features it is possible to write modern, clean and reliable code. All is necessary is to change habits and approaches, rather than language. Interestingly enough, changing approaches pays more than changing languages because approaches applicable to more than one language.

Top comments (0)