Monadic error handling in Java

Ivan Ivic
7 min readNov 27, 2019

--

Acknowledgments

Parts of this article (namely, of internal codebase that leverages these ideas in our company) are written in collaboration with Djordje Velickovic. Keep on Rocking, my friend!

Prelude

Fundamental laws of the Internet state that every tutorial about monads must start with the famous Douglas Crockford quote. Fortunately, this is not a monads tutorial.

If you tried to understand monads by reading internet tutorials, you’d get it.

In all seriousness, there are plenty of excellent introductory articles or books about category theory, aimed at traditional programmers — one such article that specifically targets Java developers is the one by Tomasz Nurkiewicz. In this article, I’d like to focus on monadic error handling and how adopting a small subset of larger ideas can quickly improve any given codebase.

Nothing written here will be especially new to seasoned functional programmers — in fact, if you are programming in a functional language, chances are you are already practicing these ideas, probably using core language features. If you are a Java developer, however, and your code base is littered with exceptions, you might benefit from at least considering alternative ways of handling error cases in your system.

Nothing exceptional about errors

Core idea and a sort of a necessary mindset shift is that there really is nothing exceptional about errors. User input, network failures, business rules being violated and similar stuff happening are a part of everyday life and not something that should surprise us. Given this fact, in ideal world, you’d like to treat every error case with the same effort and care as you would treat a “happy” path — and that includes leaning on your compiler to help you out with identification and handling of all cases. Java obviously has a concept of checked exceptions, that must be explicitly mentioned in a method’s signature. But, for better or worse, unchecked exceptions won at least a part of this battle, long time ago.

Checked exceptions can quickly become “spammy” — if everything is a checked exception, you’d soon have method signatures with dozens of different exceptions. Some people try to solve this with inheritance — declare a “super exception” and let clients handle “children exceptions”. But declaring and maintaining a hierarchy of checked exceptions is a lot of work, and abstraction quickly breaks down as soon as you start encoding different business rules/terms in a construct that simply wasn’t meant to be used in this way. Instead, many Java developers resorted to simply using unchecked exceptions — and in that way lost any help they might have gotten from the compiler.

Can we do a bit better than this, though? Can we have strongly-typed errors, that don’t superimpose a hierarchy on us, don’t litter our methods’ signatures and don’t force a GOTO mode upon us every time we want to check how they are handled?

Take a look at this simple piece of code, for example:

So.. many.. catches.. almost 22.

For the sake of argument, let’s ignore different abstraction layers being mixed in together and focus on the pain point of exception handling. Imagine that some of these exceptions are checked, but not all of them. Which runtime exceptions are being thrown by methods you are calling, and what are the ones you must handle right now? You have to dig into implementation to find out, and thus waste precious brain cycles.

Secondly, exceptions may break functions chaining. What if you are used to Java 8+ structures and would like to structure your code like this?

Sequences of computational steps ftw, but that’s just imho

Again, ignoring any other refactoring one might do, having functions throw checked exceptions is a no-go if you want to chain them via generic constructs like map. Furthermore, exception handling is essentially a sort of GOTO: you have a happy path right here, and then if you want to see how error cases are handled, you go to line XX, and observe the catch clause (if you can find it, since it can be anywhere in the call stack!)

Refactoring towards Result

Going by the above example, instead of exceptions we can have a sort of a tuple/pair structure — let’s call it Result — that will represent both our “success” and “error” cases. In Scala, this would be an Either class, which is a tuple of Left and Right value — it can hold either of those, but not at the same time. We can easily implement this in Java, representing Left as non-error and Right as Error value. Traditionally, it’s the other way around (Right is “right”), but just from aesthetic standpoint, I like having my non-error value first:

Result.java, for better results

Error is a simple class that wraps around an error code and message, but you can implement this part in various ways (two extremes would be having a simple string for error messages, on one hand, or a whole tree/collection representing various errors and stack trace, on the other).

Example of Error.java

As you might have already guessed, Result is a functor. There really isn’t much else to it: it has an identity and map operators that obey certain laws. Furthermore, it is a monad: it has a flatMap (bind) function, and if this API is familiar to you, it might be because you’ve already seen it in Java’s Optional, Stream, CompletableFuture or other structures (all of which are monads, by the way). Leveraging familiar structures is what’s so powerful about these ideas: you don’t have to learn a completely new public API. It’s a monad, so there’s not much to add, beyond the implementation internals (which in this case are rather trivial, anyway). You have a container holding either a Success or Error value, and that’s all there is to it.

We can change our original example and have all methods (that can fail) return Result, instead of throwing exceptions. For example, signature for method that finds data from a repository (or it’s database implementation) could look like this:

Result<Data, Error<RepositoryErrorCode>> findBy(Criteria c) { ....

Similarly, calling an external service would be:

Result<GResponse, Error<GatewayErrorCode>> send(GRequest sr) { ...

Notice that we don’t modify any values in the Result structure. It is just a container that wraps either success or error, and it’s value is in enabling refactoring of code towards series of type-safe transformational steps. How, then, would you “escape” this container and actually handle errors?

Traditionally, this is outside of the scope of monads, and is done via pattern matching. However, since Java is just getting switch expressions (a preview feature since version 12, and still disabled by default, in version 13), this is something that will be possible in the future, but not right now for most production code out there. For this reason, a couple of simple escaping methods and getters in Result class will allow us to check for errors:

You can even implement orThrow method, that will throw an Exception supplied via mapping function (function that will map from Error to Exception). This can ensure backward compatibility with APIs or parts of your codebase that still deal with (or expect) exceptions. This is the same trick as used by, say, Java’s Optional orElse() family of methods. Our example could look like this:

Mapping Errors

In real case scenario, you might want to have different errors for different situations (like you have with exceptions). In our example, Error class supports wrapping of any type T, and you can use this to wrap any enumeration with error codes, for example. Of course, since different methods can have different errors (depending on the context), you’d need some way to map from one error type to another, without breaking methods chaining. Since our Result is just an Either, we can implement “mirror images” of existing methods that apply for non-error cases:

A penultimate transformation function would be a kind of a reducing function, that takes both Right and Left values and turns them into singular value. Let’s call this function “fold”:

Often enough, errors you have inside your implementation are not of the same type (or underlying type) as those that are expected to be returned from your method, so you can use fold() and mapError()/flatMapError() constructs to nicely transform and align all types in one pass, without breaking method chaining:

Of course, as always, refactoring is your friend, and you can play around with your code, functions and types, and find a structure that would satisfy you. This example moved towards methods composition, so it is easy to extract and plug-in different implementations, all while leaning on the compiler to align types for you and help you structure your code.

Hopefully, you’ve gleaned some alternative paths and possibilities for structuring your error handling. As with most things in software engineering at the present time, we don’t have many empirical and scientific studies that deal with various ways we can handle errors in our code and benefits/drawbacks of different approaches. There are a few such studies out there, but I’m not aware of any serious meta-studies that deal with this topic. So, as always, it’s up to you to individually try out different things and see — in your own experience — whether this could help you in whatever metric you deem important in your context (be it productivity, aesthetics, less errors/bugs, better errors/messages, ease of refactoring, or something entirely different).

To me, functional/monadic error handling eases refactoring, improves understanding, and leads to logically more sound code structure. Having a program as a series of small computational steps, while using the compiler to help me align types is easier — for me, personally — than keeping the whole program in one potentially messy block, with error handling being a separate beast that I have to GOTO. Result monad helps in this regard, while encapsulating common “plumbing” code in it’s internals. Of course, as always, YMMV — but if nothing else, at least you can try out a different approach and see what works for you and your team.

References

--

--