Handling Kotlin Exceptions with Kategory – A Functional Approach
Kotlin has excited us at Spantree since we've first heard about it. As advocates for alternative languages on the JVM, we love the benefits it's brought to client and internal projects. The biggest benefit Kotlin has provided in the code I've written with it is the way it can handle exceptions and errors.
Working with libraries and APIs that throw exceptions can lead to unexpected behavior at runtime. In my experience, this mostly happens because I normally get no indication about any exceptions that I need to handle to make the code resilient when using the API. The specific exceptions that should be handled often manifest themselves at runtime when the application starts crashing. Although we can use the traditional Java-style exception handling in Kotlin, we can also use some functional programming concepts to provide APIs that give the users affordances at compile time about the possible things that might fail at runtime. We will illustrate how to use a Kotlin library called Kategory to handle Java libraries that throw exceptions and how to create our own APIs to be more type safe.
Limitations of traditional exception handling
Exception handling in Kotlin is similar to many other programming languages like Java, Ruby, C++, Python, Javascript, etc. This type of exception handling gives the user of the APIs throwing the exceptions little indication at compile time of the exceptions that might be thrown at runtime. Java experimented with checked exceptions in an effort to address this but it didn't go so well.
Case Study
Lets look at a typical example of using an http library to access a remote API to fetch some
data. We'll use okhttp
but please don't take this as a hit on the library. It is probably my
favorite http library for Java. All Java libraries that throw exceptions will behave in
the manner that we will illustrate.
A first pass at creating a function that fetches data from a remote API might look like the following:
loadingThat function will happily compile and we might even be bold enough to put it in production
(I've deployed code to production like that many times). And it might possibly work for days, months
or years without an issue. But it might also start failing within hours. We can't tell, and the code
we wrote gives us no indication that something might fail. Furthermore, okhttp's
type signature for
making an http call does not tell us what exceptions it might throw if something might go wrong.
We can draw on past experiences of having written services that interface with third party systems
via a network call to know that network issues can occur that will disrupt our services. To be able
to resiliently handle these outages we need to handle the possible exceptions that okhttp
will
throw when something goes wrong. Unfortunately, most of the times we don't know what those
exceptions are until something goes wrong in production or if we read the source code for the
third party library we are using. But reading the code for every third party library
that our application depends on is not practical. Embarrasingly, one of the things I've done
when the exceptions that might be thrown are unknown, I use the catch-all java.lang.Exception
:
While that code won't crash in production, it will fail to alert us of any issues that might be
happening that we should be dealing with. It also does not give the parts of our application
that are using scores
an opportunity to either inform the users in the front end that
something went wrong (e.g. MLB is down at the moment; We are experiencing network issues) or
try another service that might be a backup for times when the main service is down.
We can propagate the exception up the stack and that can give the parts of our business application an opportunity to handle the exceptions however they see fit:
loadingAnd we are back to the issues we mentioned earlier: the type signature gives us no indication
of what exceptions might be thrown, and every client of scores
needs to know and also throw
the Exception so that calls from higher up the stack can handle it.
Functional Exception Handling
Most statically typed functional programming languages provide two abstractions for handling errors and exceptions:Either
and Try
. For handling exceptions, we generally want to use
Try
; while Either
is for business logic errors.
We will be using kategory
to illustrate how Try
can be used to provide a more ergonomic API
for operations that might throw exceptions.
If we know that a library that we are using might throw an exception, Try
will take care
of catching that exception and returning it to the caller. We don't need to know the specfic
exception we are only concerned with it at the end of our stack when we want to do something
with the result of our operation. Refactoring scores
to use Try
:
For clients of scores
, they can know right away from reading the type signature that it is an
operation that can throw an exception, and that they need to handle it in their code. The compiler
will also complain if clients of scores
neglect to handle the failure cases. This usage of
Try
is helpful when we are interacting with Java (or Kotlin) code that throws exception but
we want to provide a more functional interface for the rest of our application to use.
Try
internally represents the failure in its Failure
subclass and the result of a successful
operation in its Success
subclass.
Using functions that return a Try
We've seen how to write a function that might throw an exception and how to use typed abstractions
to make handling these exceptions more explicit. But we have not shown how we use these functions.
Try
provides a number of helpful functions. It's important to note that while many of these look like functions in kotlin.collections
, these are all provided by Kategory's Try
.
Providing defaults with getOrElse
This is used for retreiving the value for theSuccess
subclass. It is null safe because it forces
us to provide a default value in the event that the result of the operation was a Failure
:
loading
Processing errors with fold
fold
is very similar to getOrElse
but it allows us to transform the successful value. We provide
the value for the failure case, but we also have access to the exception and we can use it to determine
the result value. The second argument to fold
take a function that has the Success value as a parameter.
loading
Continuing on with recover
recover
is similar to fold
but only allows us to map/transform the failure case. If the result
of a Try
is a Success
, then the lambda inside the fold
is ignored. This is useful when we want to
return a different value that depends on the exception that is thrown, but we do not want to transform
the Success
case.
We should also note that even though getOrElse
and recover
look similar, and both allow us to provide
a default value for error cases, recover
returns another instance of Try
while getOrElse
does not.
Safeguarding with for comprehensions
If you are familiar with Scala'sfor comprehensions
, Kategory
also implements them. However, the naming is a bit
unintuitive for beginners. You have to know what you are looking for in order to find it. We can use for comprehensions
by directly invoking the monad
factory on the Try
type. We want to use this when we need to do some operations
on the Success
value in a sequential manner:
loading
Inside the binding
we don't have to worry about handling Failure
. The operations will no-op and binding
will
return a Failure
.
The example above will block until completion, but if we want to make it non-blocking we can use a variant of recover
called
recoverWith
:
We can think of recoverWith
as a flatMap
but for the error cases. This example also introduces two new functions.
The first one is Try.pure
, this is just a constructor that is needed to satisfy some algebraic rules (that is beyond the scope of this blog).
And we can call bind
on the result of a recoverWith
(or on any Try
for that matter), which will give you the result, in this case a list of games.
To appreciate what binding
offers, we can write this using flatMap
:
I personally find the binding
example much more readable, but that is just a matter of taste.
Pattern matching with when
Lastly, we can also use Kotlin's default when
to extract the value of a Try
. For the Success
case, the value can be
accessed via the value
property in the Try
instance, and for the Failure
case, the value can
be accessed via the exception
property. One caveat to keep in mind is that if we replace one of these
cases with else
we won't be able to access the corresponding property because the compiler will
not know which sub-class it should be working with.
loading
Interesting tidbit, fold
is actually implemented internally as when
and inlined
.
Conclusion
It is easy to overlook exception handling when we are working with an operation that might throw an exception because we might not know what those exceptions might be, or we are not forced to think about it from the start. By using concepts from functional programming thatKategory
brings to Kotlin
, we write more reliable code when working with libraries
that might throw an exception.
References:
- https://medium.com/@stestefanfan/functional-error-handling-in-scala-309f5cca6dbb
- http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/
- http://longcao.org/2015/06/15/easing-into-functional-error-handling-in-scala
- Thanks to Paco Estevez, core contributor to the Kategory project, for explaining
Try
and providing the non-blocking example usingrecoverWith
andbinding
in the Kotlin slack.