Functional Error Handling


When dealing with errors in a purely functional way we try as much as we can to avoid exceptions. Exceptions break referential transparency and lead to bugs when callers are unaware that they may happen until it’s too late at runtime.

In the following example we are going to model a basic program and go over the different options we have for dealing with errors in Arrow. The program simulates the typical game scenario where we have to shoot a target and series of preconditions need to be met in order actually shot and hit it.


  • Arm a Nuke launcher
  • Aim toward a Target
  • Launch a Nuke and impact the Target


/** model */
object Nuke
object Target
object Impacted

fun arm(): Nuke = TODO()
fun aim(): Target = TODO()
fun launch(target: Target, nuke: Nuke): Impacted = TODO()


A naive implementation that uses exceptions may look like this

fun arm(): Nuke = throw RuntimeException("SystemOffline")
fun aim(): Target = throw RuntimeException("RotationNeedsOil")
fun launch(target: Target, nuke: Nuke): Impacted = Impacted

As you may have noticed the function signatures include no clue that when asking for arm() or aim() an exception may be thrown.

The issues with exceptions

Exceptions can be seen as GOTO statement given they interrupt the program flow by jumping back to the caller. Exceptions are not consistent as throwing an exception may not survive async boundaries, that is to say that one can’t rely on exceptions for error handling in async code since invoking a function that is async inside a try/catch may not capture the exception potentially thrown in a different thread.

Because of this extreme power of stopping computation and jumping to other areas, Exceptions have been abused even in core libraries to signal events.

at java.lang.Throwable.fillInStackTrace(
at java.lang.Throwable.fillInStackTrace(
- locked <0x6c> (a sun.misc.CEStreamExhausted)
at java.lang.Throwable.<init>(
at java.lang.Exception.<init>(
at sun.misc.CEStreamExhausted.<init>(
at sun.misc.BASE64Decoder.decodeAtom(
at sun.misc.CharacterDecoder.decodeBuffer(
at sun.misc.CharacterDecoder.decodeBuffer(

They often lead to incorrect and dangerous code because Throwable is an open hierarchy where you may catch more than you originally intended to.

try {
  doExceptionalStuff() //throws IllegalArgumentException
} catch (e: Throwable) { //too broad matches:

Furthermore exceptions are costly to create. Throwable#fillInStackTrace attempts to gather all stack information to present you with a meaningful stacktrace.

public class Throwable {
    * Fills in the execution stack trace.
    * This method records within this Throwable object information
    * about the current state of the stack frames for the current thread.
    Throwable fillInStackTrace()

Constructing an exception may be as costly as your current Thread stack size and it’s also platform dependent since fillInStackTrace calls into native code.

More info in the cost of instantiating Throwables and throwing exceptions in generals can be found in the links below.

The Hidden Performance costs of instantiating Throwables

  • New: Creating a new Throwable each time
  • Lazy: Reusing a created Throwable in the method invocation.
  • Static: Reusing a static Throwable with an empty stacktrace.

Exceptions may be considered generally a poor choice in Functional Programming when:

  • Modeling absence
  • Modeling known business cases that result in alternate paths
  • Used in async boundaries over unprincipled APIs (callbacks)
  • In general when people have no access to your source code

How do we model exceptional cases then?

Arrow provide proper datatypes and typeclasses to represent exceptional cases.


We use Option to model the potential absence of a value

When using Option our previous example may look like:

import arrow.*
import arrow.core.*

fun arm(): Option<Nuke> = None
fun aim(): Option<Target> = None
fun launch(target: Target, nuke: Nuke): Option<Impacted> = Some(Impacted)

It’s easy to work with Option if your lang supports Monad Comprehensions or special syntax for them. Arrow provides monadic comprehensions for all datatypes for which a Monad instance exists built atop coroutines.

import arrow.typeclasses.*

fun attackOption(): Option<Impacted> =
  binding {
    val (nuke) = arm()
    val (target) = aim()
    val (impact) = launch(target, nuke)


While we could model this problem using Option and forgetting about exceptions we are still unable to determine the reasons why arm() and aim() returned empty values in the form of None. For this reason using Option is only a good idea when we know that values may be absent but we don’t really care about the reason why. Additionally Option is unable to capture exceptions so if an exception was thrown internally it would still bubble up and result in a runtime exception.

In the next example we are going to use Try to deal with potentially thrown exceptions that are outside the control of the caller.


We use Try when we want to be defensive about a computation that may fail with a runtime exception

How would our example look like implemented with Try?

fun arm(): Try<Nuke> =
  Try { throw RuntimeException("SystemOffline") }

fun aim(): Try<Target> =
  Try { throw RuntimeException("RotationNeedsOil") }

fun launch(target: Target, nuke: Nuke): Try<Impacted> =
  Try { throw RuntimeException("MissedByMeters") }

As you can see by the examples below exceptions are now controlled and caught inside of a Try.

// Failure(exception=java.lang.RuntimeException: SystemOffline)

// Failure(exception=java.lang.RuntimeException: RotationNeedsOil)

Unlike in the Option example here we can fold over the resulting value accessing the runtime exception.

val result = arm()
result.fold({ ex -> "BOOM!: $ex"}, { "Got: $it" })
// BOOM!: java.lang.RuntimeException: SystemOffline

Just like it does for Option, Arrow also provides Monad instances for Try and we can use it exactly in the same way

import arrow.typeclasses.*

fun attackTry(): Try<Impacted> =
  binding {
    val (nuke) = arm()
    val (target) = aim()
    val (impact) = launch(target, nuke)


While Try gives us the ability to control both the Success and Failure cases there is still nothing in the function signatures that indicate the type of exception. We are still subject to guess what the exception is using Kotlin when expressions or runtime lookups over the unsealed hierarchy of Throwable.

It turns out that all exceptions thrown in our example are actually known to the system so there is no point in modeling these exceptional cases as java.lang.Exception

We should redefine our functions to express that their result is not just a Nuke, Target or Impact but those potential values or other exceptional ones.


When dealing with a known alternate path we model return types as Either Either represents the presence of either a Left value or a Right value. By convention most functional programing libraries choose Left as the exceptional case and Right as the success value.

We can now assign proper types and values to the exceptional cases.

sealed class NukeException {
  object SystemOffline: NukeException()
  object RotationNeedsOil: NukeException()
  data class MissedByMeters(val meters : Int): NukeException()

typealias SystemOffline = NukeException.SystemOffline
typealias RotationNeedsOil = NukeException.RotationNeedsOil
typealias MissedByMeters = NukeException.MissedByMeters

This type of definition is commonly known as an Algebraic Data Type or Sum Type in most FP capable languages. In Kotlin it is encoded using sealed hierarchies. We can think of sealed hierarchies as a declaration of a type and all it’ s possible states.

Once we have an ADT defined to model our known errors we can redefine our functions.

fun arm(): Either<SystemOffline, Nuke> = Right(Nuke)
fun aim(): Either<RotationNeedsOil, Target> = Right(Target)
fun launch(target: Target, nuke: Nuke): Either<MissedByMeters, Impacted> = Left(MissedByMeters(5))

Arrow also provides a Monad instance for Either in the same it did for Option and Try. Except for the types signatures our program remains unchanged when we compute over Either. All values on the left side assume to be Right biased and whenever a Left value is found the computation short-circuits producing a result that is compatible with the function type signature.

import arrow.core.extensions.either.monad.binding

fun attackEither(): Either<NukeException, Impacted> =
  binding {
    val (nuke) = arm()
    val (target) = aim()
    val (impact) = launch(target, nuke)

We have seen so far how we can use Option, Try and Either to handle exceptions in a purely functional way.

The question now is, can we further generalize error handling and write this code in a way that is abstract from the actual datatypes that it uses. Since Arrow supports typeclasses, emulated higher kinds and higher order abstractions we can rewrite this in a fully polymorphic way thanks to MonadError


MonadError is a typeclass that allows us to handle error cases inside monadic contexts such as the ones we have seen with Either, Try and Option. Typeclasses allows us to code focusing on the behaviors and not the datatypes that implements them.

Arrow provides the following MonadError instances for Option, Try and Either

import arrow.core.extensions.option.monadError.*

// arrow.core.extensions.option.monadError.OptionMonadErrorKt$monadError$1@4a21a4af

import arrow.core.extensions.`try`.monadError.*

// arrow.core.extensions.try.monadError.TryMonadErrorKt$monadError$1@36f2329a

import arrow.core.extensions.either.monadError.*

// arrow.core.extensions.either.monadError.EitherMonadErrorKt$monadError$1@6487ea1f

Let’s now rewrite our program as a polymorphic function that will work over any datatype for which a MonadError instance exists. Polymorphic code in Arrow is based on emulated Higher Kinds as described in Lightweight higher-kinded polymorphism and applied to Kotlin, a lang which does not yet support Higher Kinded Types.

fun <f> arm(ME: MonadError<F, NukeException>): Kind<F, Nuke> = ME.just(Nuke)
fun <f> aim(ME: MonadError<F, NukeException>): Kind<F, Target> = ME.just(Target)
fun <f> launch(target: Target, nuke: Nuke, ME: MonadError<F, NukeException>):
  Kind<F, Impacted> = ME.raiseError(MissedByMeters(5))

We can now express the same program as before in a fully polymorphic context

fun <F> MonadError<F, NukeException>.attack():Kind<F, Impacted> =
  binding {
    val (nuke) = arm<F>()
    val (target) = aim<F>()
    val (impact) = launch<F>(target, nuke)

Or since arm() and bind() are operations that do not depend on each other we don’t need the Monad Comprehensions here and we can express our logic as:

fun <F> MonadError<F, NukeException>.attack1(ME): Kind<F, Impacted> =
  ME.tupled(aim(), arm()).flatMap(ME, { (nuke, target) -> launch<F>(nuke, target) })

val result = Either.monadError<NukeException>.attack()
// or
val result1 = Either.monadError<NukeException>.attack1()

Note that MonadError also has a function bindingCatch that automatically captures and wraps exceptions in its binding block.

fun <f> MonadError<F, NukeException>.launchImjust(target: Target, nuke: Nuke): Impacted {
  throw MissedByMeters(5)

fun <f> MonadError<F, NukeException>.attack(): Kind<F, Impacted> =
  bindingCatch {
    val (nuke) = arm<F>()
    val (target) = aim<F>()
    val impact = launchImpure<F>(target, nuke)

Example : Alternative validation strategies using ApplicativeError

In this validation example we demonstrate how we can use ApplicativeError instead of Validated to abstract away validation strategies and raising errors in the context we are computing in.


import arrow.*
import arrow.core.*
import arrow.typeclasses.*

sealed class ValidationError(val msg: String) {
  data class DoesNotContain(val value: String) : ValidationError("Did not contain $value")
  data class MaxLength(val value: Int) : ValidationError("Exceeded length of $value")
  data class NotAnEmail(val reasons: Nel<ValidationError>) : ValidationError("Not a valid email")

data class FormField(val label: String, val value: String)
data class Email(val value: String)


sealed class Rules<F>(A: ApplicativeError<F, Nel<ValidationError>>) : ApplicativeError<F, Nel<ValidationError>> by A {

  private fun FormField.contains(needle: String): Kind<F, FormField> =
    if (value.contains(needle, false)) just(this)
    else raiseError(ValidationError.DoesNotContain(needle).nel())

  private fun FormField.maxLength(maxLength: Int): Kind<F, FormField> =
    if (value.length <= maxLength) just(this)
    else raiseError(ValidationError.MaxLength(maxLength).nel())

  fun FormField.validateEmail(): Kind<F, Email> =
    map(contains("@"), maxLength(250), {
    }).handleErrorWith { raiseError(ValidationError.NotAnEmail(it).nel()) }

  object ErrorAccumulationStrategy :
  object FailFastStrategy :
  companion object {
    infix fun <A> failFast(f: FailFastStrategy.() -> A): A = f(FailFastStrategy)
    infix fun <A> accumulateErrors(f: ErrorAccumulationStrategy.() -> A): A = f(ErrorAccumulationStrategy)


Rules defines abstract behaviors that can be composed and have access to the scope of ApplicativeError where we can invoke just to lift values in to the positive result and raiseError into the error context.

Once we have such abstract algebra defined we can simply materialize it to data types that support different error strategies:

Error accumulation

Rules accumulateErrors {
    FormField("Invalid Email Domain Label", ""),
    FormField("Too Long Email Label", "nowheretoolong${(0..251).map { "g" }}"), //this accumulates N errors
    FormField("Valid Email Label", "")
  ).map { it.validateEmail() }

Fail Fast

Rules failFast {
    FormField("Invalid Email Domain Label", ""),
    FormField("Too Long Email Label", "nowheretoolong${(0..251).map { "g" }}"), //this fails fast 
    FormField("Valid Email Label", "")
  ).map { it.validateEmail() }


Tutorial adapted from the 47 Degrees blog Functional Error Handling