Recently, I've got big discussion on functional-language approach - Either
. For me this is natural step forward, after ditching Java Exceptions. I've always seen Exceptions as something unnatural, indicating something serious going on with JVM internals. When I've discovered Either
my life changed, and below you can find my view on why it is better than exceptions.
What is Either?
Concept of Either
is widely used in Scala as first-class citizen. Basically, this is a Tuple where as a rule of thumb we consider left value as an error, and right value as an valid result. In Java we have no Either
by default, but it can be included through very popular vavr
library. Example of such construct realised by vavr
you can see below:
public Either<String, Integer> divide(int first, int second) {
if (second == 0) {
return Either.left("Could not divide by 0");
}
return Either.right(first/second);
}
...
var result = divide(1,2);
if (result.isLeft()) {
System.out.println(result.getLeft());
} else {
System.out.println("Result of division is: "+result.get());
}
One-way comuunication between layers
We are used to structure our application code in layers. No matter if it is Clean Architecture or classic MVC, we need to take care that communication is done only within one direction. Going with exceptions, we may miss that constraint.
Let's take as an example simple method which divides two numbers. If user tries to divide by 0 we need to stop them from doing so. In classic Spring approach we will do the following:
- Check if value passed is 0
- Throw an
ArithmeticException
- Catch the exception either in
@Controller
or@ControllerAdvice
through@ExceptionHandler
- Prepare response which is needed to be returned for the customer
If only someone will catch the same exception in other place, it can result by unexpected message recieved by consumer. We don't know which handler will take care of that. Also, such @ExceptionHandler
may be deleted by accident, and noone may even notice that something is wrong as compiler will not notify us. Main issue is, that exception handling is different flow than calling the method. In this case higher layer need to introduce separate part of code, which will be handling messages thrown from lower layer. This breaks one-direction rule.
In oposition to that, we can try with Either
approach:
- Check if value passed is 0
- Return left with defined response class containing error message
- Return directly from called method appropriate invalid state.
The contract is clear from the beginning. We know upfront what type of parameters we need to pass, and what can we get in return. If ever will be any breaking changes in contract, we will see them explicitly in all classes which are using above method. We don't have to create separate place, to cover fail scenarios.
Multiple points for handling one method
When calling method, we expect it to return result. It is easier for us knowing, that such result could be either success or failure. Using Either
approach, we're able to determine which part of it was successful, and which was not. Someone new coming to the project, and looking at the method header is able to get to know what they can expect. By looking at below example, we know what type of success and error can we expect after calling function.
Either<DivisionError, DivisionResult> divide(int first, int second)
It is not so clear with exceptions. If we're facing checked exception being thrown, we still can know what to expect. It may be less elegant to handle (as we need to use try {...} catch (...)
block) but at least we know that such exception can be thrown and we need to handle it. With unchecked exceptions it is even worse. Using method we need to know that something can happen, and prepare for it. If we're exposing an endpoint, we need to remember to write appropriate @ExceptionHandler
which will cover such exception, and return human-readable result for the consumer. If we won't do that, it may lead to some strange messages being returned. Customer can come back angry, that we're returning them some crap.
Testing method
Again - unchecked exception is the hardest to handle here. Do you know about black-box testing technique? It is intended to check something only basing on the externals we can see. When method is throwing unchecked exception, we will not even know that something can go wrong. Explicit contract indicates that there might be negative and positive scenario (left and right part of Either
) which makes tests easier.
Also, we can even test it farily easy without access to test helping frameworks. I know - JUnit is a standard nowadays. So by few simple assertions we can check if result was successful or not. In case of exceptions - we need to prepare test setup in a way that error is being caught. If we want to do it really precise, we need to know all types of exceptions which can be thrown.
Code cleanness
This is purely subjective topic, so this may not apply to everyone.
When creating code, I like to read it from top to bottom. Like a book, you can expect what comes next. Having try {...} catch (...)
block some part of the story is moved to other place withing the page. Having unchecked exception being thrown, story is moved to some random place, where it is handled.
In case of either, you have everything in one place. You can check straight away if the result is successful or not, process it in a pipeline like in Streams or fold the result to prepare return value.
So, should I drop every Exception in code and replace it with Either?
No, definitely not! It is always bad idea to make big-bang change and do it everywhere. My recommendation is to try it out on small subset of new changes, and start introducing it bit by bit if you like it. Like I've mentioned, in Scala this construct is first-class citizen, so I hope one day we will not have to have separate library for it.
If you want some code example with comparison of (unchecked) Exception and Either approach, you can find it here.
Top comments (0)