A More Elegant Alternative to Golang's Error-Handling
Recently, I joined a new project at Colorkrew that uses Golang as its backend language. Almost everyone on the team, including myself, had no experience with Golang, so we had to start from zero. Learning Golang has been a rewarding experience so far. After all, it is one of the most in demand programming languages now. However, writing code in Golang is not all sunshine and roses. Unopinionated as I am, I still have my fair share of complaints, the biggest being the recommended way of error handling in Golang.
According to Golang’s official tutorial, when errors occur, functions are supposed to return them, along with the regular returned values. Here is an example from Golang’s tutorial with slight adjustments:
package main
import (
"errors"
"fmt"
"log"
)
// Hello returns a greeting for the named person.
func Hello(name string) (*string, error) {
if name == "" {
return nil, errors.New("empty name")
}
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return &message, nil
}
The Hello
function takes a string called name
as a parameter. It creates a message
using the given name
and returns it. name
being an empty string is unacceptable and considered an error. The Hello
function first checks whether the name is an empty string. If the string is empty, then Hello
returns nil
and an empty name
error. Otherwise, it returns an actual message constructed with the name
parameter and an empty error (nil
).
Since Golang requires an error to be returned to the caller of a function as a part of the function’s returned value, Golang engineers have to return an error up, layer by layer, until they want to handle it. As a result, Golang code is inevitably replete with the following repetitive error boilerplate.
if err != nil {
return nil, err
}
Such an error-handling style differs from standard programming language conventions, where functions raise errors to have them dealt with in catch blocks. The mainstream convention does not require engineers to pass errors up layer by layer. Engineers can freely catch and handle errors at whichever layer they want resulting in a less verbose and less repetitive codebase.
In Golang’s defense, its error-handling philosophy requires engineers to decide where and how to handle every error. This decreases the probability of system failures due to programmers’ oversight. Also, treating an error as a part of a function’s returned value is conducive to engineers writing pure functions, which are more predictable than impure functions. Raising errors causes the program to deviate from the normal flow and create side effects, thus making the code less predictable. Returning errors eliminates this issue and makes it easier to write pure functions, producing code that engineers can easily reason about.
The more I copied and pasted the above error boilerplate, the more I wondered: is there a more elegant way to handle errors without violating the principles of functional programming? Then, one day, it dawned on me that the answer to this question is quite simple: use Either
, a data type commonly used for error handling in functional programming languages such as Scala.
What on earth is Either
? Either
is a disjoint union, and an instance of it can be either Left
or Right
. Conventionally, we would store error information in Left
and valid values in Right
. In other words, when an Either
is a Left
, it is an error containing an error message. When an Either
is a Right
, it contains a valid value.
Let’s rewrite the above Hello
function in Scala with Either
.
def hello (name: String): Either[String, String] = {
if (name.isEmpty()) {
return Left("empty name")
}
return Right(s"Hi, $name. Welcome!")
}
// In Scala, you can also omit the return keyword and rewrite the Hello function as below:
// def hello (name: String): Either[String, String] = {
// if (name.isEmpty()) {
// Left("empty name")
// } else {
// Right(s"Hi, $name. Welcome!")
// }
// }
The first noticeable difference is that the Scala hello
function has only one returned value, as opposed to the Golang function’s two. Secondly, we can be certain the Scala hello
function returns one of two possible types : Left
with an error message or Right
containing a valid welcome message. We do not have the same degree of certainty when invoking the Golang Hello
function. According to its definition, there are only two possible combinations of returned values: (nil, error)
and (message, nil)
. But technically there are two more possible combinations we can infer from the type hint: (nil, nil)
and (message, error)
. When calling the Golang Hello
function, we need to reason about all four possible combinations, which adds more complexity than Scala’s Either
.
In addition to simplifying return values of functions, Either
also streamlines error handling. Let’s expand on the previous example. We want to build a new function VIPHello
that takes a parameter called name
, and calls the Hello
function with it. If the Hello
function returns an error, VIPHello
returns that error. If the Hello
function returns a valid message, VIPHello
appends additional text to the message and returns the resulting message. In Golang, we would implement it as follows.
package main
import (
"errors"
"fmt"
"log"
)
// Hello returns a greeting for the named person.
func Hello(name string) (*string, error) {
if name == "" {
return nil, errors.New("empty name")
}
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return &message, nil
}
func VIPHello(name string) (*string, error) {
helloMessage, err := Hello(name)
if err != nil {
return nil, err
}
message := fmt.Sprintf("%v Please accept our sincere gratitude", *helloMessage)
return &message, nil
}
As you can see, we have to use the error boilerplate I mentioned above. Imagine how verbose your code becomes if you have to copy and paste the error boilerplate each time you invoke a fallible function. Meanwhile, we can implement the same function in Scala more elegantly, thanks to Either
and its map
method.
def hello (name: String): Either[String, String] = {
if (name.isEmpty()) {
return Left("empty name")
}
return Right(s"Hi, $name. Welcome!")
}
def vipHello (name: String): Either[String, String] = {
return hello(name).map((right) => s"$right Please accept our sincere gratitude")
}
A simplistic yet readable (to Scala programmers) one-liner! Let me explain how it works. If the returned value of the hello
function is a Left
, the map
method would simply return it. The magic of Either’s map
method happens when the returned value of the hello
function is a Right
. It extracts the valid message from the Right
, appends the additional message to it as specified by the function passed to it, and wraps the resulting message with Right
before returning it. This function achieves everything its Golang counterpart does but more cleanly and elegantly. More importantly, there is no need to copy and paste the error boilerplate everywhere!
Thank you for listening to my rant about my dissatisfaction with Golang’s error-handling style! I’m still enamored with many other aspects of Golang, such as its simplicity and outstanding performance, but I wish I didn’t have to keep copying and pasting the godforsaken error boilerplate everywhere or write functions that return valid values and errors separately.
I hope this article can inspire talented Golang programmers to rethink Golang’s error-handling style and potentially explore the possibility of introducing Either
to Golang.