When a Golang nil error is not nil

  • YC YC
  • |
  • 01 September 2024
post-thumb

Coding in Golang are usually fun and easy for developers that are new to the language. There are sometimes gotcha which will catch out even experience developers of the language. One of the most intriguing gotchas I have recently encountered at work is that an error checking of a nil error ends up being non-nil.

Take for example you want to create a custom error object that adds more details to an error.

type CustomError struct {
	err error
	errorCode int
}

func (c *CustomError) Error() string {
	return c.err.Error()
}

Now you have an inner function that returns your custom error.

func innerFunction() *CustomError {
	// Do something
	return nil
}

Your outer function conveniently uses a helper function to print and handle the error if it is not nil

func printErrorIfNotNil(err error) {
	if err != nil {
		fmt.Printf("Error found : %v", err)
	}
}

func outerFunction() {
	err := innerFunction()
	printErrorIfNotNil(err)
}

So far everything looks good and logical, right?

Let’s run the full code and you will expect to see no errors printed.

Run it on go playground

package main

import "fmt"

type CustomError struct {
	err error
	errorCode int
}

func (c *CustomError) Error() string {
	return c.err.Error()
}

func innerFunction() *CustomError {
	// Do something
	return nil
}

func printErrorIfNotNil(err error) {
	if err != nil {
		fmt.Printf("Error found : %v", err)
	}
}

func outerFunction() {
	err := innerFunction()
	printErrorIfNotNil(err)
}

func main() {
	outerFunction()
}

Result:

Error found : <nil>
Program exited.

What? Error found but error is <nil>? What sorcery is this?!

This gotcha is clearly explained in the official Golang doc.

An interface value is nil only if the V and T are both unset, (T=nil, V is not set), In particular, a nil interface will always hold a nil type. If we store a nil pointer of type *int inside an interface value, the inner type will be *int regardless of the value of the pointer: (T=*int, V=nil). Such an interface value will therefore be non-nil even when the pointer value V inside is nil.

In simpler terms let’s look at our example. In innerFunction(), we return nil on a function that returns *CustomError. This means assigning nil value to *CustomError. We use this as an argument to a function that takes in error interface. Now this means we are storing a nil pointer of type *CustomError inside an interface value, which means this interface is no longer nil regardless of what value is the pointer.

To prevent this gotcha, I strongly recommend to always return error type whenever you design your function API signature instead of custom error types. This improves interoperability between your code and also consumers of your packages and eliminates ambiguity.

func innerFunction() error {
	// Do something
	return nil
}

Check out the working code Run it on go playground

package main

import "fmt"

type CustomError struct {
	err       error
	errorCode int
}

func (c *CustomError) Error() string {
	return c.err.Error()
}

func innerFunction() error {
	// Do something
	return nil
}

func printErrorIfNotNil(err error) {
	if err != nil {
		fmt.Printf("Error found : %v", err)
	}
}

func outerFunction() {
	printErrorIfNotNil(innerFunction())
}

func main() {
	outerFunction()
}

Another interesting way to use custom errors is via sentinel errors in Golang. Sentinel error are unaffected by this gotcha as we are creating a custom errors using constant instead of pointers. You can embed additional information as part of your sentinel errors by wrapping them and returning them as error type.

comments powered by Disqus

You May Also Like