FSharp Error Handling Compared
Matt Gallagher has written a great blog post comparing error handling in 8 different languages, as an FSharp enthusiast, I thought it would be interesting to see how the language stacks up.
Before we start our discussion, read over this article from Raymond Chen.
It’s hard to write good error-code-based code since you have to check every error code and think about what you should do when an error occurs.
It’s really hard to write good exception-based code since you have to check every single line of code (indeed, every sub-expression) and think about what exceptions it might raise and how your code will react to it.
This is especially true on the dotnet core platform as all exceptions are unchecked exceptions. So not only can functions throw exceptions, but you don’t even have to handle them, or even worse, wrap the function calls in a try/catch (exception ex) block and simply forget they every happened.
The FSharp implementation
This is what I came up with as an FSharp implemention for the error handling comparison between the 8 languages.
Points of interest
Let’s go though the code now and point out what’s interesting about the FSharp implementation.
- (Line 3–5) Creating types in fsharp is easy, here we’re creating an exception type that wraps an integer. A good first rule for breaking free from a csharp mindset, don’t be afraid to create types!
- (Line 7–10) Notice how we’re able to declare our custom exception as as possible return value from our function. As all cases in a discriminated union must be handled, the caller knows exactly what to expect from our function and handle them appropriately.
- (Line 12–13) Simple pattern around the ugly modulus check that makes checking for even or odd simple to read.
- (Line 15–22) This is the function that throws our exception and you’ll notice it’s wrapped in a lazy constructor. Why is it wrapped in a lazy constructor? Becasue fsharp evaluates functions eagerly and without lazy our exception gets thrown at the time the function is bound. It doesn’t matter that we’ve wrapped the function in nice try/catch block, our program bombs out with an uncaught excpetion.
- (Line 24–30) Catching our excpetion and returning our nice return result that easy for the client to handle.
Eager evaluation and exceptions don't mix
When writing this simple example, it took me a while to grok while my program kept crashing out, at one point i even thought dotnet core on macOS must be broken! Hahahaha, we never like to the think the problem is of our own making ;)
It finally occurred to me that the EvenTimeValue function was being evaluated at the time the program started, when the function was bound, and not when I called the function. Wrapping the call in a lazy constructor fixed the issue, but it begs the question does eager evaluation and runtime exceptions play well together? Would i have had the same problem with a return result?
I agree with Raymond Chen, writing good exception based code is really hard, throw into the mix an eager evaluating functional language, and writing good exception based code becomes really, really hard. Exceptions can happen all over the place, when function are bound, with no way of catching them.
Pattern matching is your friend
The cpsEvenTime function that wraps the exception and returns possible results is easy for a client to handle with a simple pattern match of the results. In fact, fsharp forces a user to handle all the options when matching on a discriminate union.
This function is explicit about what may happen when you call upon it’s functionality. You may succeed an get your even integer, or you may fail for myriad of reasons that you may recover from.
Is the evenTimeValue function that throws exceptions explicit about what may happen when you call upon it’s functionality? There’s not contract, no explicitness, no friendliness in the call.
In fact, you can merrily ignore the failure result until your program blows up. Even worse, maybe some random try/catch block in your program was catching excpetion and the problem has been suppressed for the rest of time.
Maybe it’s the old c++ COM programmer in me, but i don’t mind handling problems when I make function calls. Discriminated unions and pattern matching in fsharp make handling the possible outcomes of a function call easy and complete.
I don’t see this same ease and completeness from the current exception model and that’s a problem.