Home golang - errors are easy, but at what cost?
Post
Cancel

golang - errors are easy, but at what cost?

It’s simple, right?

Go is simple, and error handling should be equally simple. In other language such as Python or Ruby, you raise Exceptions if something goes wrong and catch them inside some sort of try/except block. In Go, if anything downstream of your function could error, you’re expected to return two values: one value that sometimes represents the object you’re trying to return, and a second value that sometimes represents the fact that something went wrong.

Sometimes?

Let’s look at an example of simple error handling in Go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func getUser(username string) (string, error) {
	// connect to some database and get user
	fullname, err := db.Find(username)
	if err != nil {
		return "", fmt.Errorf("getUser: failed to find user: %w", err)
	}
	return fullname, nil
}

func main() {
	userToFind := "lcrownover"
	fullname, err := getUser(userToFind)
	if err != nil {
		log.Fatalf("failed to find user %s: %w", userToFind, err)
        os.Exit(1)
	}
	fmt.Println("user's full name:", fullname)
}

Seems pretty straightforward, right? If the user is found, the error value is simply nil. If it can’t find the user, you return… an empty string? Okay, I guess that makes sense. Go expects you to return the zero-value of whatever type you’re returning, so an empty string it is. We’ll just gloss over the fact that you’re at the mercy of writing error checks every few lines. I wonder if career Go developers will eventually develop PTSD around having to constantly type

1
2
3
if err != nil {
	return SomeZeroValue, fmt.Errorf("extra context: %w", err)
}

but that’s just part of the job I guess.

So you deploy, and everything’s working fine, until you need more than a username. What if you also need their full name? Maybe other pieces of information? So you decide to refactor and encapsulate this user data into a User struct, then pass that around.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type User struct {
	Username string
	FullName string
	PartyPooper bool
}

func getUser(username string) (User, error) {
	// connect to some database and get user
	user := User{Username: username, PartyPooper: true}
	fullname, err := db.Find(username)
	if err != nil {
		return user, fmt.Errorf("getUser: failed to find user: %w", err)
	}
    user.FullName = fullname
	return user, nil
}

func main() {
	userToFind := "lcrownover"
	user, err := getUser(userToFind)
	if err != nil {
		log.Fatalf("failed to find user %s: %w", userToFind, err)
	}
	fmt.Printf("is %s fun at parties?: %v\n", user.FullName, user.PartyPooper)
}

Let’s say you’re searching for Alyssa, but she won the lottery last month and HR finally removed her account. The return values for getUser will be a mostly-filled-out User object and an error. The User could easily be mistaken for a valid user object, it’s only missing one field. If you didn’t if err != nil it, have fun tracking down where the hell this partially-provisioned struct came from.

If you Google for “golang return nil struct”, hoping for a solution to this issue, you’ll find that there is an alternative, returning nil pointers and relying on panics to uncover issues.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type User struct {
	Name string
	Friends []*User
}

func getUser(username string) (*User, error) {
	name, err := db.Find(username)
	if err != nil {
		return nil, err
	}
	user := &User{
		Name:    name,
		Friends: []*User{},
	}
	return user, nil
}

If you return a pointer to User, you then have the option of returning nil, err if there’s an error. This is quite a bit more straightforward, as you will never end up with partially-created structs. The issue then becomes, what happens later when I forget to write if err != nil and try to access user.Name? Panic!

If you’re reading this looking for a clever solution, try looking elsewhere. Or go look at Rust’s error handling, cause they have their shit figured out.

This post is licensed under CC BY 4.0 by the author.