Functional Error Handling

Balraj Singh - Jan 24 '23 - - Dev Community

Till now we have been mostly talking about Functional Concept like Composition and Currying with some practical usage. But for a change, I thought to pick a topic which developers encounter in an everyday scenario.

Exceptions in OOP centric languages
Throwing an exception and handling them in a try catch block is something which every developer encounter quite often. But if u forget to handle an exception "all hell breaks loose" at runtime. This becomes one of the most commons reasons for an application to crash in production or become unresponsive. Making your application looks bad.

But Why would I forget to handle an exception?
An excellent question. But a simple answer. Exceptions are implicit.
Few points which make exception lead to the incorrect and dangerous code are:-
The compiler doesn't complain if you don't handle it. Owing to exceptions implicit nature in every sense.
Function signature might tell us that a function can throw an exception with a keyword called as throws in Java or Swift but doesn't clearly explain what kind of exception to handle.
Runtime Exceptions like nullpointerexception or dividebyzeroexception are generated by wrong data. Hence completely impossible to find and capture upfront and avoid any production crashes.
Exceptions feel more like a GOTO statement given they interrupt the program flow by jumping back to the caller.
Exceptions are not reliable as throwing an exception may not survive an async block.

Functional Way To Handle It
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.
Functional Paradigm provides many Data Types which help to handle errors in the code. Let's take a look at these Data Types.

Option
Option is used to model an absence of a value. In Swift, all primitive types or custom types have no null value. To make a value nullable you have to explicitly mention it as nullable as follows:-
var obj: Optional<String>

var obj: String? // This is a short hand notation for representing // optional value in Swift
Now let's see how can we use optional values:-
func doSomething() -> String?
func doSomething2(value: Stirng) -> String?
// To use the above function
// We can use guard or if
guard let value1 = doSomething(),
let value2 = doSomething2(value: value1) else {
return
}
// Use value2 as final result now
print(value2)

There is a lot of boiler-plate code the above example. This can drastically reduce if your programming supports Monadic Comprehension or specific Syntax for it. 
Optional is Functor and Monad hence it has a definition of map and flatMap. This helps to rewrite the above code in a more precise and elegant way. Let's see that now:-
func doSomething() -> String?
func doSomething2(value: Stirng) -> String?
// To Use the above function
// Use Monadic Comprehension
doSomething()
.flatMap(doSomething2(value:))
.map { x in print(x) }

In the above code snippet looks more precise and readable with all repeated boiler-plate removed. This is the power of Monadic Comprehension.
While we could model the problem using Option and forgetting about exceptions we are still unable to determine the reasons why doSomething() and doSomething2() 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.

Try
Try is a data type similar to Option but used when we want to be defensive about a computation that may fail with a runtime exception.
Let's continue to take our above example of 2 functions and see how it looks when use Try. You can find the definition of Try and it's supporting functions here.
func doSomething() -> Try<String>
func doSomething2(value: Stirng) -> Try<String>
doSomething()
.flatMap(doSomething2(value:))
.fold({ err in
print(err)
}, { result in
print(result)
})

The above example uses 2 functions. The first one is flatMap which comes as a part of Monadic nature of Try Datatype. The second one is a fold method which takes a Try value and applies either failure block or success block depending on the value of Try input. This Try is a more composable and explicit to handle than the try-catch block we have been using since our childhood.
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 kind of exception is been used.

Either
When dealing with the alternate path we model the return type as Either. Either represents a Left value or a Right value. By convention, most Functional libraries choose Left as the exceptional case and Right as the success value.
Result Data Type is a specified scenario of Either where Left side is replaced by Error type and Right is of type T.
Implementation of Either and its helper function can be found here.
Let's take the above example and see how to implement using Either Type.
func doSomething() -> Either<InValidParsingError, String>
func doSomething2(value: Stirng) -> Either<InValidParsingError, String>

This type of definition is commonly known as an Algebraic Data Type or Sum Type in most FP capable languages. We can see that the return type is quite explicit in defining what is the return Success result value type is and what type of error can be expected to return.
Either can be utilized like Option & Try as it implements Monadic instance. Let's see how to use Either for the above scenario:-
doSomething()
.flatMap(doSomething2(value:))
.either(onLeft: { error in print(error) },
onRight: { result in print(result) })

We have seen so far how we can use Option, Try and Either to handle exceptions in a purely functional way.
With functional approach to solve Error handling we can clearly see that our code has become more explicit in defining what kind of errors can be expected. Since the error is returned hence we can handle it beyond multiple threads. This way of error handling is more natural and doesn't break the flow of the code like a GOTO statement. It also gives a possibility to compose function in case of errors also. All in all, this seems to be more reliable, explicit and composable.
In the future, we will see more scenarios like these where Functional concepts can make your life easier in everyday coding. Till then stay tuned!!!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .