{{announcement.body}}
{{announcement.title}}

Consistent Error Propagation and Handling in Java

DZone 's Guide to

Consistent Error Propagation and Handling in Java

Learn more about consistent error handling and propagation in Java.

· Java Zone ·
Free Resource

Learn more about consistent error handling and propagation in Java.

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

The problem has existed since the birth of the first computer program, and software engineers invented many ways to deal with errors!

You may also like: 9 Best Practices to Handle Exceptions in Java

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

  • Return a special value (oftentimes, a 'null' value is used for this purpose)
  • Throw an exception

Both of these approaches have significant drawbacks.

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

Exceptions are quite expensive compared 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 the following text, I'll try to avoid FP-specific terminology. This does not make the approach less functional, but it simplifies the concept for those who are not yet 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. When it is used for error propagation, then, by convention, the first (or "left") type is used to represent error type, while the second (or "right") type represents return value type.

In code, this looks like:

Java




xxxxxxxxxx
1


 
1
Either<ErrorDetails, UUID> parseUUID(final String input) {
2
       ...
3
       // failure
4
       return Either.left(ErrorDetails.of("Unable to parse UUID"));
5
       ...
6
       // success 
7
       return Either.right(uuid);
8
   }



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

A deeper look exposes a lot of advantages:

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

The code above demonstrated the "producing" side. Now, let's take a look at what the "consuming" side looks like:

Java




xxxxxxxxxx
1


 
1
...// Service interface
2
   Either<ErrorDetails, User> getUserById(final UUID uuid);
3
 
          
4
   ...//Actual use
5
   return parseUUID(parameter).flatMapRight(service::getUserById); 



This suspiciously simple code contains everything necessary to handle errors:

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

Specializing in Narrow Use Case

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

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

The 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 regards to both sides. When Either<L, R> is used for error handling, this requires the consistent application of some convention, like the one mentioned above.

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

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

Java




xxxxxxxxxx
1
14


 
1
...
2
 
          
3
   Result<UUID> parseUUID(final String input) {
4
       ...
5
       return Result.failure(ErrorDetails.of("Unable to parse UUID"));
6
       ...
7
       return Result.success(uuid);
8
   }
9
 
          
10
   ...// Service interface
11
   Result<User> getUserById(final UUID uuid);
12
 
          
13
   ...
14
   return parseUUID(parameter).flatMap(service::getUserById); 



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

Adapting Existing Code to use Result<T>

The 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 nulls. So, we need a convenient way to interact with existing code.

For this purpose, the Result<T> implementation in Reactive Toolbox Core provides a set of helper methods that allow wrapping traditional methods into ones returning Result.

The example below shows how these helper methods can be used:

Java




xxxxxxxxxx
1
11


 
1
interface PageFormattingService {
2
       Result<Page> format(final URI location);
3
   }
4
 
          
5
   private PageFormattingService service;
6
 
          
7
   private Result<Page> formatPage(final String requestUri) {
8
       return lift(URI::create)
9
               .apply(requestUri)
10
               .flatMap(service::format);
11
   } 



Wrapping Up

This article is an attempt to describe some main concepts of the Reactive Toolbox Core library. Of course, none of these concepts are new. I'm just trying to create a library that 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 a modern, clean, and reliable code. All is necessary to change habits and approaches, rather than the language. Interestingly enough, changing approaches pay more than changing languages because of approaches applicable to more than one language.

Further Reading

Exceptions in Java: You're (Probably) Doing It Wrong

Making an Exception-Handling Mechanism

9 Best Practices to Handle Exceptions in Java

Topics:
java ,tutorial ,error ,error handling ,propagation ,functional programming

Published at DZone with permission of Sergiy Yevtushenko . See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}