Skip to content

This blog investigates the error types in golang and summerizes a best practice for using errors.

Error types

Sential error

Sential error is an error we predefine and return it when we need it like this:

// this is an example of the sentinel error
var ErrNotFound = errors.New("not found")

// can be returned this way
return nil, ErrNotFound

// can be added with additional context in case needed
return nil, fmt.Errorf("some operation: %w", ErrNotFound)

Error type

  • key: type assert and get data

Error type is define our own error type and implement the error interface. This is usually used for carry some useful information for the callstack and expose error from package.

type SyntaxError struct {
    msg    string // description of error
    Offset int64  // error occurred after reading Offset bytes
}

func (e SyntaxError) Error() string { return e.msg }

So when we get an error from one of functions in the package, we can use type assert to fetch the data inside it if it’s a predicted error type.

Opaque error

  • key: type assert and decide workflow

Opaque error means we define our own error type, but not expose them. Instead, we expose the interfaces the error might implement. This mainly aims to decide the workflow and we don’t care the data inside the error.

  • For example:
type temporary interface {
        Temporary() bool
}

// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
        te, ok := err.(temporary)
        return ok && te.Temporary()
}

type timeoutError struct{}
func (e *timeoutError) Error() string   { return "i/o timeout" }
func (e *timeoutError) Temporary() bool { return true }

type rateLimitError struct{}
func (e *timeoutError) Error() string   { return "rate limited" }
func (e *timeoutError) Temporary() bool { return true }

// this error when checked by IsTemporary(), will always return false
// since it doesn't satisfy the temporary interface
type parseError struct{}
func (e *timeoutError) Error() string   { return "parse error" }

The net package also define their own opaque error interface like this:

// An Error represents a network error.
type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

Note that it’s better to use opaque error when we have more than 1 case should be check, or using type error.

Pointer receiver or value receiver?

Error is an interface, all structs implement it could be an error type:

type error interface {
    Error() string
}

Basically, we will define our error type with a value receiver or a pointer receiver:

type valueError struct {
    info string
}

func (e valueError) Error() string {
    return e.info
}

type pointerError struct {
    info string
}

func (e *pointerError) Error() string {
    return e.info
}

Prerequisities knowledges

Value passed by interface is a reference

Interface values are represented as a two-word pair giving a pointer to information about the type stored in the interface and a pointer to the associated data.

func cleanupDir(storage StorageAPI, volume, dirPath string) error {
  // [...]
     entries, err := storage.ListDir(volume, entryPath)
  // [...]
}


type magicI struct {
  tab *_typeDef
  ptr *retryStorage
}

func cleanupDir(storage magicI, ...) error {
  // [...]
    // we're trying to call (*retryStorage).ListDir()
    // since what we have is a pointer, not a value.
    entries, err := storage.ptr.ListDir(...)
  // [...]
}

Compiler work for function receiver

For methods with value receivers, the compilers will apply a pointer method implicitly like:

func (p *T) Something(...) (...) {
  v := *p
  return v.Something(...)
}

But for the method with pointer receiver, compiler does nothing as:

The method set of the corresponding pointer type *T is the set of all methods declared with receiver *T or T (that is, it also contains the method set of T).

Better to use pointer receiver

We can always use pointer receiver for Error method and benefit from no copy cost and no generated function cost.

If we see errors.New :

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
}

And fmt.Errorf :

func Errorf(format string, a ...any) error {
    //...
    if p.wrappedErr == nil {
        err = errors.New(s)
    } else {
        err = &wrapError{s, p.wrappedErr}
    }
    return err
}
type wrapError struct {
    msg string
    err error
}

func (e *wrapError) Error() string {
    return e.msg
}