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:
To help solve the repetition,
Rob defined a type called
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
which provides the same error handling as
while satisfying the
Using it changes the example into this:
Rob closes by stating:
Using the language
The above solution is specific to
even though the same error handling strategy
makes sense for the
and from the blog post we know that it is in fact repeated
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.
Let’s start by considering
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
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.
With a sufficiently expressive type system
(and using completely made up syntax)
we could express this as a type
which represents the result of a computation
and covers two cases:
- we either have some value of type
Aif 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
but as soon as one step fails we want to stop.
Let’s define a method
Then for this task:
r contains an error,
Then just returns
otherwise it calls
f and returns the result of that call,
which is exactly what we wanted.
So far so good.
You may have noticed that calling
simply discards the value of
(if it’s a successful
We also don’t need
Then to always return
Result containing the same type of value.
Let’s lift those restrictions and generalize the method:
f is still free to ignore its argument
or to return a
Result containing a value of type
and that a
Result containing an error
Result<A> for any type
Using this type (and a
Write method that returns it),
the example code from above could look something like this:
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.
Then we defined above is well known
in certain circles that practice functional programming,
only those folks usually call it
That’s because the
Result type is a monad.
(Some other names for similar types are
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.
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.
Two commonly perceived problems of Go are that handling errors is verbose and repetitive and that parametric polymorphism is unavailable.
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,
Bmay be different from
Abut 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. ↩
Russ Cox provides a reasonable explanation ↩