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.