By Eric Driussi
Recently I’ve been collaborating with a team in a decently large and complex Java codebase with more than a couple of years in production.
The codebase has gone through various business changes and has quite a lot of edge cases to consider.
Most of the code was done by people that are no longer in the company and.
Consequently, defensive programming is quite prevalent.
As you might expect, Exceptions are thrown everywhere, and the code is riddled with try/catch
blocks where the catch
blocks effectively act as if/else
statements,
This, in turn, makes maintenance and feature development a slow and painful process.
I’ll concede that handling error cases with Exceptions (or similar constructs in other langs) doesn’t necessarily lead to a disastrous try/catch
situation, but to be honest I feel it’s so easy to screw up that I almost expect it to happen.
The danger here is, to put it briefly, that you can easily end up with two semi-parallel execution flows in your mind while reading the code.
Let me explain.
While parsing the try
block, all that logic depends not only on what you are actually reading, but also on each possible point of failure along the way.
Each line you read might stop the main execution flow and sending you who knows how many lines below (or files up the stack).
It’s like a less fancy (and less dangerous) GOTO
statement, or a weird multi-branch if
statement which cannot be refactored to Guard Clauses and whose corresponding else
might be in a different file.
It requires you to reason about what a given function returns and what it might throw differently, which is strange since conceptually they are both ways to communicate an operation result up the stack.
A common “solution” to this problem (especially in Java teams) is something like “just let Spring’s @ExceptionHandler
handle it”, which of course is (IMHO) significantly worse.
Not seeing the problem doesn’t mean it’s not there, it just means you are blind to it.
A lot of questions were raised regarding how to improve the design, how to minimize these try/catch
blocks or how to make the edge cases clearer.
Just like a codebase filled with null checks is (usually) begging for the use of Optional, one with lots of complex try/catch
could usually be improved by adding the Either type into the mix.
However, these two are not (in my experience) equally easy to fit into a developer’s toolkit if they haven’t dabbled into functional paradigms before.
This is especially the case with Java, since Optionals are built in but Eithers are not, and Java devs are usually a lot more weary of dependencies than say, the average JavaScript dev.
Plus, Optionals are conceptually easy to grasp: either there is a thing or there is not. It’s close enough to a null
(which we are all painfully familiar with), an easy step to take.
Eithers, on the other hand, require you to think rather differently about error management, even more so if you are not used to handling errors by returning them.
They also (usually) come with a bunch of clever sounding functions you can use
and concepts you need to grasp.
It’s not immediately clear what you should do with them, and they can feel esoteric at first.
Let’s see if we can build a custom solution, halfway between OOP and functional.
At a very basic level, we’ll need something that represents the result of an operation.
This piece of code should contain (wrap) both the success and the failure of the operation.
Since this won’t necessarily evolve into a fully fledged Either, we could call it Result
.
This naming is closer to its intended use case and should (hopefully) deter any functional snob from politely informing us that it’s not technically a proper Either type.
It might look something like this:
public class Result<S, E> {
private final S success;
private final E error;
private Result(S success, E error) {
this.success = success;
this.error = error;
}
public static <S, E> Result<S, E> success(S success) {
return new Result<>(success, null);
}
public static <S, E> Result<S, E> error(E error) {
return new Result<>(null, error);
}
}
Very simple, very basic.
At some point, we will need to check whether the result is an error.
We will also need to unpack the result to maybe show it to the user, or include it in an API response, for example.
public boolean isSuccess() {
return success != null;
}
public boolean isError() {
return error != null;
}
public S getSuccess() {
return success;
}
public E getError() {
return error;
}
Depending on how the layers of our software are designed, we could have the need to map one error/success into another.
Our errors might be layer specific and our concept of success might mean a different thing in the persistence layer compared with the domain layer.
public class Result<S, E> {
private final S success;
private final E error;
private Result(S success, E error) {
this.success = success;
this.error = error;
}
public static <S, E> Result<S, E> success(S success) {
return new Result<>(success, null);
}
public static <S, E> Result<S, E> error(E error) {
return new Result<>(null, error);
}
public boolean isSuccess() {
return success != null;
}
public boolean isError() {
return error != null;
}
public S getSuccess() {
return success;
}
public E getError() {
return error;
}
public <T> Result<T, E> mapSuccess(Function<S, T> fn) {
return Result.success(fn.apply(success));
}
public <T> Result<S, T> mapError(Function<E, T> fn) {
return Result.error(fn.apply(error));
}
}
Here we have to bring in functions as parameters, but other than that, there is nothing fancy about this code. Boring code is good, Grug likes boring code.
You don’t have to force yourself (or others) to fully go the functional way to reap the benefits from using the bits you actually need.
Just add more capabilities to your Result type as you need them and, if you feel it’s getting unwieldy, ditch it completely in favor of a third party solution.
Add to this some sort of Error
interface to ensure type safety and proper mapping, and you are well on your way to minimize thrown Exceptions.
Or don’t! You can start by just having strings on both sides and add fancier types as needed.
Of course, this will not prevent third party libs or even your lang’s standard lib from throwing Exceptions.
We can decide to not throw them within the code we write (which is already more
than half the battle), but a lang that provides these constructs will always require you to defend yourself from them.
If a piece of software is reasonably designed, this is relatively easy to deal with:
Wrap your I/O code in a try/catch
, build a Result
accordingly, and handle
that from that point on.
A simple example would be a piece persistence layer code that gets data from a DB.
Obviously the connection could fail, the data might not be found, you might expect one result and get more than one, etc.
Take the code that talks to the DB and wrap it in a try/catch
.
Build your Result
from the Exceptions you catch (or might catch), return it to the caller and forget about Exceptions from that point on.
This approach also allows for more flexibility when failure and success are not quite black and white.
Say for instance that your persistence code didn’t find the Data.
That’s just a not found for the persistence layer, but at the application layer the story might be different.
Depending on the use case, that might imply that invalid info was provided for the lookup or that the data was moved or that it should be created without user input.
Map the Result
as needed to convey this information between the layers of your software.
You could add a fancier map
function to freely map errors to successes or vice versa if needed.
When you reach the piece of code in charge of dealing with these errors and/or presenting them to the user, simply unpack the Result
and paint the relevant information (or build the API response, or start your retry policy, you do you).
Exceptions are a pain to get right, and sometimes it’s easier to just avoid them as much as possible.
You don’t have to re-write the codebase or teach your team “the way of the function” to achieve this.
Keep it simple, add just the code you need as you need it.
Make the code work for your team, not the other way around.
Do you want more? We invite you to subscribe to our newsletter to get the most relevan articles.
If you enjoy reading our blog, could you imagine how much fun it would be to work with us? let's do it!
But wait a second 🖐 we've got a conflict here. Newsletters are often 💩👎👹 to us. That's why we've created the LEAN LIST, the first zen, enjoyable, rocker, and reggaetoner list of the IT industry. We've all subscribed to newsletters beyond our limits 😅 so we are serious about this.