Errors are values
In his post “Errors are values”, Rob Pike, one of the original authors of Go, attends the common perception that one must repetitively type
in order to handle errors.
He recounts an encounter of his with another Go programmer who had some code that looked like this:[1]
To help solve the repetition,
Rob defined a type called errWriter
with a write
method
that stops writing to w
as soon as it hits the first error:
This encapsulates the repetitive error handling and lets them simplify the above code to something like this:
He then notes that this pattern appears often
in the Go standard library,
including in the bufio.Writer
class
which provides the same error handling as errWriter
above
while satisfying the io.Writer
interface.
Using it changes the example into this:
Rob closes by stating:
Use the language to simplify your error handling.
Using the language
The above solution is specific to io.Writer
,
even though the same error handling strategy
makes sense for the io.Reader
type,
and from the blog post we know that it is in fact repeated
in bufio.Scanner
and the archive/zip
and net/http
packages.
Go does not support parametric polymorphism (or “Generics”), but if it did we could use it to write a single implementation of this error handling pattern and reuse it for different types.
Let’s check out what that might look like.
Result
Let’s start by considering io.Writer
.
It is an interface with exactly one method:
That method’s return type is a pair consisting of a value and an error,
where in the common case that error is nil
,
indicating that the operation finished successfully.
If an error did occur, the value may still be present (and non-zero),
but we’ll ignore that case for this blog post.[2]
With a sufficiently expressive type system
(and using completely made up syntax)
we could express this as a type Result<A>
, say,
which represents the result of a computation
and covers two cases:
- we either have some value of type
A
if the computation was successful - or we have some error (of type
error
) if the computation failed
An implementation could look similar to this:
This type provides a place to put the error handling strategy we’re after:
From a successful Result
we want to run the next “step” of our program,
which may itself return a Result
,
but as soon as one step fails we want to stop.
Let’s define a method Then
for this task:
If r
contains an error, Then
just returns r
,
otherwise it calls f
and returns the result of that call,
which is exactly what we wanted.
So far so good.
Polishing
You may have noticed that calling Then
simply discards the value of r
(if it’s a successful Result
).
We also don’t need Then
to always return
a Result
containing the same type of value.
Let’s lift those restrictions and generalize the method:
Note that f
is still free to ignore its argument
or to return a Result
containing a value of type A
[3],
and that a Result
containing an error
satisfies Result<A>
for any type A
.
Using this type (and a Write
method that returns it),
the example code from above could look something like this:[4]
I’ll be the first to admit that this piece of code is not elegant, but I believe that that’s due to lack of support by the language, so let’s consider where improving that could get us.
The M-Word
The method Then
we defined above is well known
in certain circles that practice functional programming,
only those folks usually call it flatMap
(or bind
).
That’s because the Result
type is a monad.
(Some other names for similar types are
Result
,
Either
,
Or
,
Xor
, or
\/
.)
And in languages with better support for monads we can more easily express computations using them. This is what the same piece of code looks like in Haskell:
Yes, this code performs the exact same error handling as our examples above. Other code that handles errors the same way will also look the same, reusing the error handling strategy defined in the result type, removing the need to wrap facades around interfaces every time we want to handle the errors they generate.[5]
In the end we accomplished exactly what is being preached for Go: We treat errors as values, and we are using our language to simplify our error handling. The difference is that we only have to do that once.
Conclusion
Two commonly perceived problems of Go are that handling errors is verbose and repetitive and that parametric polymorphism is unavailable[6].
One of the authors of Go offers a solution to one of those problems, but his advice boils down to “use monads,” and because of the other problem you cannot express this concept in Go.
This leaves us having to implement artisanal one-off monads for every interface we want to handle errors for, which I think is still as verbose and repetitive.
-
Omitting the slice expressions because they’re irrelevant to our discussion ↩
-
The case is easy to model but distracts from the point of the examples. ↩
-
Just like regular parameters,
B
may be different fromA
but doesn’t have to be. ↩ -
We’re following the convention of calling unused parameters
_
↩ -
You may think that this fixes the error handling strategy of each function by way of its return type, but there are ways to parameterize those as well. ↩