Go Puzzlers: Comparing Error Values

John Montroy
Go Puzzlers
Published in
5 min readNov 27, 2022

In Go, errors are values, as in they have a value and a type, like anything else. This is different from languages like Java or Python, where errors are exceptional things (pun intended) that require special handling and language syntax, like try / catch.

Since errors are just values in Go, we handle them by comparing them to other values. There are some subtleties here — thus, this article.

#1 —Comparing new errors directly

What does this code print out?

// Go Playground: https://play.golang.com/p/f_1lmY16aKY
e1 := errors.New(“ohno”)
e2 := errors.New(“ohno”)
// What does this print?
fmt.Println(e1 == e2)

Answer: false

We’re starting simple — all errors created via errors.New are distinct. You might know this intuitively or from reading and writing Go code, but why exactly is this true?

Short answer: these errors e1 and e2 are ultimately compared as pointers. Pointers are only equal in Go if they point to the same underlying object in memory. Since each call to errors.New is a new allocation (thus the name), the pointers are not equal.

It DOES NOT MATTER that the errors have the same string value “ohno” within them.

Long answer? Let’s take a little detour into how interfaces and comparisons work in Go. The details will aid us in subsequent puzzlers.

The code for errors.New (as of Go 1.19) is as follows:

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

A few things to note:

  1. First off, the docs on the function tell you that all values returned are distinct. Always read the docs.
  2. For callers, errors.New returns an error interface.
  3. The actual thing returned is of type *errorString , which implements the error interface using a pointer receiver on the Error() method.

So when we call the function twice, we get two error interface values implemented by an *errorString inside them. We then compare our two error interface values. What does Go do when comparing two interface values? Let’s check the Go Programming Specification, specifically its section on Comparison Operators:

Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.

What are dynamic types and values? We once again delegate to the spec:

The static type (or just type) of a variable is the type given in its declaration, the type provided in the new call or composite literal, or the type of an element of a structured variable. Variables of interface type also have a distinct dynamic type, which is the (non-interface) type of the value assigned to the variable at run time (unless the value is the predeclared identifier nil, which has no type).

So here, for errors.New:

  1. The static type is error.
  2. The dynamic type is *errorString.
  3. The dynamic value is &errorString{s: "ohno"}.

So two interface values are equal if they have identical dynamic types and equal dynamic values. Well, our two errors e1 and e2 certainly have identical dynamic types: *errorString. Do they have equal dynamic values? That would mean comparing the pointer values &errorString{s: "ohno"}. Let’s reference the spec again:

Pointer values are comparable. Two pointer values are equal if they point to the same variable or if both have value nil. Pointers to distinct zero-size variables may or may not be equal.

Well, there you have it. The dynamic values are both separately-allocated struct pointers, e.g. &errorString{s: "ohno"}. The pointers point to different memory / different variables. So they are not equal.

#2 — Comparing new errors by value

Per the original blog post introducing them:

The errors.Is function compares an error to a value.

So we start by comparing two errors as values using errors.Is. What does this code print out?

// Go Playground: https://play.golang.com/p/Kl8d9MpWhc_T
e1 := errors.New("ohno")
e2 := errors.New("ohno")
// What does this print?
fmt.Println(errors.Is(e1, e2))

Answer: false

Yes. This is good. errors.Is should not change the fact that we’re comparing two distinct variables as values. Those variables are two distinct interface values that contain two distinct pointers to structs as their dynamic values.

#3— Comparing existing errors as values

What does this code print out?

// Go Playground: https://play.golang.com/p/cpimPBzUeOS
var (
ErrCustom = errors.New("ohno")
)

var e1 error = ErrCustom
var e2 error = ErrCustom
// What does this print?
fmt.Println(e1 == e2)
// And this?
fmt.Println(errors.Is(e1, e2))

Answer:

This is good. Both variables are error interface values that contain and point to the same underlying error in memory. So this matches everything we want, and is indeed akin to how we compare error values most of the time.

Note, however — this is a bit contrived. In normal code, we’d usually compare some error returned by a function to a package-level variable. But this mimics the general idea.

#4— Comparing custom errors as values

Let’s try one more thing with values. What does this code print?

// Go Playground: https://play.golang.com/p/8DGtBOuTQSp
type CustomError struct {
Err string
}

func (ce CustomError) Error() string {
return ce.Err
}

func main() {
var e1 error = CustomError{Err: "ohno"}
var e2 error = CustomError{Err: "ohno"}
fmt.Println(errors.Is(e1, e2))
}

Answer: true!

Can you see what happened? We’ve created a custom error type, which implements the error interface using value receivers. That means that variables of this type actually ARE compared by value!

Recall that errors.New returns an error interface value containing an *errorString dynamic type and value. It’s returned with a *errorString because that’s how the Go library writers chose to implement error for the type — using pointer receivers. And when we compare pointers, they have to point to the same thing to be equal. This was clearly an intentional choice of semantic — errors.New should return something that is distinct / not equal to any other error, every time! Otherwise they probably would’ve called it something else.

So now we have a custom error type, which is assigned to and contained within an error interface value. But these dynamic values implement the interface with value receivers, which means they get compared not as pointers, but just as structs! And per the Golang spec again:

Struct values are comparable if all their fields are comparable. Two struct values are equal if their corresponding non-blank fields are equal.

Well, the only fields here are strings, and strings are also compared directly by content:

String values are comparable and ordered, lexically byte-wise.

So these custom errors are equal.

What would happen if we changed our custom error to implement error using pointer receivers? Well, that would just make our situation completely identical to the original *errorString scenario, no? And we know what happens there!

Conclusion

We’ve looked at how to compare Go errors by value, mostly using == and errors.Is. We haven’t looked at:

  • Comparing Go errors by type using errors.As
  • Comparing wrapped Go errors

We’ll tackle these subjects next time.

--

--