Consistent Error Propagation and Handling in Java
Learn more about consistent error handling and propagation in Java.
Join the DZone community and get the full member experience.
Join For Free
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:
xxxxxxxxxx
Either<ErrorDetails, UUID> parseUUID(final String input) {
...
// failure
return Either.left(ErrorDetails.of("Unable to parse UUID"));
...
// success
return Either.right(uuid);
}
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:
xxxxxxxxxx
...// Service interface
Either<ErrorDetails, User> getUserById(final UUID uuid);
...//Actual use
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:
xxxxxxxxxx
...
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);
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:
xxxxxxxxxx
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);
}
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
Published at DZone with permission of Sergiy Yevtushenko. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Extending Java APIs: Add Missing Features Without the Hassle
-
Auditing Tools for Kubernetes
-
The SPACE Framework for Developer Productivity
-
Effortlessly Streamlining Test-Driven Development and CI Testing for Kafka Developers
Comments