Working with my team, I had to deal with some business errors which were Exceptions. I was a bit nervous about them, and was having a “not good enough”, “not expressive enough” feeling. Having a Scala background, I decided to show them how we could handle errors differently.
Let’s look at how we are generally dealing with errors in Java. We’ll see what are the downsides, and why’s important to properly handle all of our errors in a more type-safe way.
We’ll work from a base solution and improve it using vavr and some of its functional types: Either, then a combination of Either and Option while keeping a fluent style.
Finally, we’ll work at combining our different error types into a more global type, but it’s not going to be straightforward.
In Java, it’s common to throw Exceptions and just deal with errors “later” in some try/catch up upon the hierarchy. It’s a common practice to just throw or convert to RuntimeException to avoid adding throws on every functions — which s a bad smell anyway. Often, the “later” is the main function, and the program will simply exit.
Exceptions in constructor
The worst part is throwing an exception from a constructor. We don’t have the choice but to return an instance of the class here: Exceptions are the only way to escape this. It’s definitely a bad smell: a constructor can’t . This is where we should use a “factory method”.
If we want to check for the nullity and to ensure a given pattern, we can do:
It’s also common to see projects using Preconditions to ensure arguments are not null or respect some business rules. If they don’t, Preconditions will just throw an IllegalArgumentException or similar.
Either this or that
Coming from Scala, I’m used to use Either to enforce the management of the errors. The compiler will not compile if I don’t explicitely deal with it. It’s like forgetting to add throws or a try/catch in Java to deal with checked exceptions.
We are going to see step by step the improvements we can apply to make our code more robust and type-safe.
The initial state
We want to represent a Country as an proper entity in our program instead of having a String everywhere. The command-line gives us the initial String that we want to turn into a Country asap. We enforce a business rule to ensure the country code looks like an iso3 (“FRA”, “POL”).
To use it, somewhere, we need to wrap it into a try-catch to handle the Exception:
A factory method
I don’t want to throw exceptions in a constructor. A constructor should not process anything, contains no business rules, nor do side-effects. It just sets the instance properties from the parameters. Therefore, we’ll move the error management into a proper factory method, for isolation purpose.
We must not forget the set our constructor as private, to ensure everybody to use the factory method, otherwise that would defeat its purpose.
Now we have a proper type to return from our simili-constructor. Right now, it’s still Country, but we’ll quickly change that to be more expressive.
Compiler help us
The compiler won’t enforce us to deal with this exception because it’s a RuntimeException. And that’s bad. We are just totally ignoring, discarding, and throwing the error to the trash.
Errors are part of the code. Errors are not meant to be ignored, they are meant to be deal with. Errors should be explicit. A code is way more read than written.
WORM: Write Once Read Many.
Help everybody by expressing the possible outcome of the functions, similar to throws for checked exceptions but without its downsides.
Why throws is not good enough
- it follows a different path than the return type, why having 2 tracks?
- no polymorphism
- implementing an interface enforces we to add the throws declaration of exceptions we are often not even throwing. Edit: this is false, an implementation doesn’t have to throws the same Exceptions (which is a bummer so, why do we even have throws on interfaces therefore?) but it can’t add new throws. We say interface is a “contract” but up to a point.
- if your implementation needs to throw an exception not present in the interface, you must wrap it into a RuntimeException because you can’t add it in
throws, that’s not good.
- Lambdas can’t throw checked
Exceptions but only unchecked ones, because all the functional interfaces don’t
throwsanything. But lambdas are because ubiquitous with Java 8, meaning you are just hiding tons of errors in your code. This is why https://github.com/pivovarit/throwing-function or https://projectlombok.org/features/SneakyThrows exist.
- to let the code “clean”, a lot of people cast them into RuntimeException defeating their purpose
- no composition
- it has to be Exception: what if I prefer to have a custom class not related to Exception?
throws is clearly to throw out. We know the compiler type-checks the program to ensure it makes sense. We must rely on it to help us ensuring we handle our errors properly.
A return type with error
We must express that our function can fail. Because a function already returns some result A, we must express the fact that it can also return another type E (an Exception or another class). We want our functions to be total: we know its outcome by reading the types. It’s basically a tuple, but a tuple doesn’t offer us any “smart” functions. There is a way better type to express this: Either.
So Either is a “smart tuple” polymorphic in 2 types: Either<E, A> and contains two sides: a Left (E) and a Right (A). Left and Right are called projections of Either.
Either is not part of the JDK. We’ll use http://www.vavr.io/ which is a Java library containing some functional types to make the development more robust.
- Failures are on its left side: Either.Left(new Exception(“boo”))
- Successes are on its right side: Either.Right(42)
Both of those expressions can be represented by a single type: Either<Exception, Integer> (that would be the return type of the function)
There are other such types to express errors: Try<A>, Validated<E, A> or even Option<A>. Each has different “qualities” and drawbacks. We won’t explore them here, but feel free to take a look at their documentation.
If we adapt our code with Either, that gives:
Our Country constructor is now free of logic and just acts as a wrapper of values.
The Exception is part of the return type of the factory method. We must deal with it now: we don’t have access to the Country directly because it may not be available.
We can be “smart” and unwrap the Country from the Either by throwing an exception (here, the left part) by using
That gives us the same behavior as before but it’s the caller choice now to throw the exception, not the callee choice.
But this is not what we should do because we just lost track of our error here: it became a flying Exception in our program again. If we have a part of our program that depends on the Country, we can call it only if we are sure we have a Country available. This is where we must use
What happened here, our result type is now Either<Exception, Boolean>! Where is our Country?
map passed the Right part (Country) to
flyTo that returns a
boolean. Hence the Right part became a
Boolean. This is how we can deal with errors without losing them.
What if our
flyTo could itself crash due to some errors? This is where the composition of Eithers is interesting. When we want to
map our Either with a function that itself returns an
Either, we must use
mapand flatten the given result otherwise we would get
Either<Exception, Either<Exception, Distance>>.
This code handles two errors path and one success path that gives a Distance at the end.
Being fluent with Options and Eithers
Let’s go back to our factory method to add more logic: we must ensure our iso3 exists and we want to grab its corresponding iso2. The JDK has the list available through
We’ll combine more FP constructions here:
List (from vavr, not java.util) to adopt a fluent style. Let’s show the code and explain:
- We still have our initial regex check of 3 characters
- We loop through all ISOCountries (they are iso2):
- We only keep (
filter) the iso2 which has the corresponding iso3 we are looking for. This still returns a
- We grab the first element if it exists: this returns an
Option<A>a simpler type than Either: it can be projected to
None. It also contains
flatMapto transform the value if it exists.
- If it exists, we build our Country from it with our iso3 and iso2 Strings, that returns an
- To match the function signature, we transform our
Either<Exception, Country>by providing the Left (used if the
Tada, we handle all possible failures explicitely, no exception are thrown, no if/else, no free variables, only lambdas. It’s a fluent way of coding: just follow the path, there is no alternate track.
Notice that we don’t care our errors are of type Exception because we are not throwing them. We should substitute them to a proper type of ours to be more specific and expressive:
We are returning a proper subset of errors, explicitely reduced to the smallest scope, which has nothing to do with Exceptions.
It’s always a good thing to reduce to the maximum what the types can do. It’s easier to reason about, and it’s less bug-prone.
If you’re a bit into Scala, I suggest you to take a look at my other article that goes deeper into this subject (Connascence, Free Theorems, Typeclasses…):
Types: Never commit too early
A few days ago, I was answering to a tweet from @kubukoz (https://twitter.com/sderosiaux/status/1027653596404436993):
Combining types of errors
The difficulty arises when we need to combine several types of errors.
Let’s say we have our
flyTo that handles its own error type
Now, we can’t compose them with
First, it does not compile. Second, what would be our error type? We have two of them! Several solutions:
FlyErrorinherits a common base such as
BusinessError. We could have several “main type of errors” we could inherit from. You’ll need to do that for all your error types, because we never know when you’ll need to combine them.
- When it’s time to handle it (like in the root of your application), you’ll probably need to distinguish between those types. Languages often offers pattern matching to do that. It’s the equivalent of a bunch of if/else checking for the type of class and casting it, to adapt the treatment.
- Combine them into an Either itself! That gives something more complicated in Java:
We are mapping the errors to Either with
mapLeft, each on a distinct side. We must help the Java compiler to find the good type, hence the hint in between.
Even Scala does not offer us a nice solution on a silver plate:
It’s shorter and clearer just because we are using the type inference and cats library (to provide
leftMap in the for-comprehension).
But Either won’t scale. What we are looking for is a co-product (also called a sum-type or disjoint union type — Either looks like one) to represent a super-type of all our errors without inheritance and without particular order. In Scala (there is no such thing in Java), using shapeless, we could do:
Combining errors can land us in a complex territory. This is why we often have a base Error type or we are just using Exception as base class.
We saw that Errors are better handled with Eithers because error management is made explicit into the code. It is type-checked by the compiler, we can’t just forget it.
We often only care about the “happy-path” where everything works as expected. A program has way more “not-happy-paths” (errors) to deal with. Thus it’s important to build around errors to make a program robust.
Functional Programming libraries have several abstractions available to work with errors: Either, Try, Option, Validated. Thanks to them, we can always work with a fluent style and avoid “side tracks”. Those types can always be combined in some ways, useful when different function return types needs to be mixed-in. This combination forces the developer to be explicit about the error handling. It’s not just forsaken to the caller and its caller and so on.
This is easier for anyone to read the code and know about the possibilities of failures, and how they are handled. This makes the code maybe not more readable (especially when several types of error must be handled), but more robust because explicit. We are not going to forget to handle an error during a refactor: the compiler will warn you!
Is it better to write vavr-ified code than idiomatic Java code? I’m sure yes.
Thanks for reading.
If you like what you read, you can read more on my website: https://www.sderosiaux.com/