20 November 2015
The Happy path of writing software is usually the easiest part and coincidentally is the the thing focused on most during estimation. A lack of thought behind error handling can have big consequences on the long term health of a code base.
I am going to contrast on the 3 main methods of handling errors that I have come across, exceptions, product types (i.e tuples) and sum types (Try, Either, et al).
The problem with exceptions
On the face of it exceptions seem convenient but most people understand that you need to apply a lot of good practice for them not to be a pain. There is countless literature about the misuses of exceptions, from Pokemon exception handling to using exceptions as control flow.
The best use-case for exceptions is when you “know” it can’t be recovered from. The problem is exceptions tend to be conflated with normal errors that you do want to recover from.
When you use exceptions like this then it impacts code re-use. By throwing exceptions you are losing referential transparency, which means you cannot trust the type system;
getFoo might not return a
Foo for all inputs (it is not a total function). This means that reasoning your code becomes more difficult (because you need to check the source to see the real behaviour) and it is harder to simply plug functions together.
Product types to the rescue?
The debate around error handling is fairly prominent in the GoLang community, mainly because it doesn’t really* have exceptions.
The language supports tuples so the convention is to simply return the error to the caller if there is one.
In this case we are referentially transparent, the higher order function lets us know that it wants a function which could return an error and will act on it.
This explicitness means you don’t have to look into the implementations of functions to check for exceptions being thrown and the compiler (and tooling) will complain if you try and pretend a function could never return an error.
There is a better way
In simple examples it doesn’t seem so bad, but a lot of Go codebases are littered with these kind of checks. The main problem is that it is very difficult to compose potentially failing functions into new functionality.
Scala (and lots of other strongly typed languages) lets you write sum-types which allow you to encapsulate these very common computations in the type system in such a way that you can be explicit and still write your code in a declarative and convenient way.
As you can see, i am declaratively gluing the functions which could fail together. If they fail at any point then the result becomes a
DomainError and doesn’t call the following functions.
It’s important to note that the respective functions don’t “care” about this composition, i didn’t have to do anything special other than declare that there might be an error using the type system.
This approach has the referential transparency of Go whilst being more convenient and declarative
When you raise errors to a first class citizen in your code by asking the compiler for help, you improve the re-usability and understandability of your code; not to mention it will be more robust as you won’t be having uncaught exceptions flying around.
As your type system becomes more expressive the perceived convenience of exceptions over explicitness disappears.