Skip to content
/ erreql Public

golang analysis for checking error types by equality instead of errors.Is

License

Notifications You must be signed in to change notification settings

R167/erreql

Repository files navigation

erreql

NOTE: This linter is funcitonal and generally works quite well, though after writing it, I learned about https://github.com/polyfloyd/go-errorlint. For production systems, I would generally recommend using that analyzer instead.

An anaylsis to check for usages of checking error types using == equality instead of errors.Is introduced in go1.13.

When passing errors in an application, it's useful to capture stack traces of where an error was encountered by wrapping the original error. However, this breaks equality checks for statically defined errors. For example:

package main

import (
  "errors"
  "runtime/debug"
)

var ErrNotFound = errors.New("not found")

func database() error {
  return ErrNotFound
}

func helper() error {
  if err := database(); err != nil {
    return WithTrace(err)
  }
  return nil
}

func controllerB() error {
  return helper()
}

func main() {
  err := controllberB()
  if err == nil {
    // untyped nil is fine to compare against
  } else if err == ErrNotFound {
    // Oh no! b/c helper() wraps the database error, we never hit this path
  } else if errors.Is(err, ErrNotFound) {
    // This is the correct way to check the underlying type of an error
  }
}

type TraceError struct {
  err   error
  stack []byte
}

func (e TraceError) Error() string { return e.Error() }
func (e TraceError) Trace() []byte { return e.stack }
func (e TraceError) Unwrap() error { return e.err }

func WithTrace(err error) error { return TraceError{err, debug.Stack()} }

However, this package makes an exception for special "sentinel error value" types which generally indicate an end of normal operations when calling the function directly. For example, io.EOF is returned to indicate the end of an input stream

// ReadLength reads upto n bytes from r
func ReadLength(n int, r io.Reader) ([]byte, error) {
	b := make([]byte, 0, n)
	for {
		c, err := r.Read(b[len(b):cap(b)])
		b = b[:len(b)+c]
		if err != nil {
      // allow sentinel values to use `==` checks
			if err == io.EOF {
				return b, nil
			}
			return b, err
		}

		if len(b) == cap(b) {
			// At capacity
			return b, nil
		}
	}
}

Sentinel values are currently defined as

  • Comparison of an identifier which implements error
  • Identifier name does NOT match ^err.|Err|Exception e.g.
    • db.ErrNotFound - use errors.Is
    • internalError - use errors.Is
    • cursor.EndOfData - sentinel value

In general, errors in golang should follow the naming pattern errName/ErrName so this case should be fairly reasonable in most scenarios, however there are edge casese to be mindful of (and note this is best effort)

var ignoredErrors = []error{
  db.ErrNotFound,
  context.DeadlineExceeded,
}

func maybeSwallow(err error) error {
  for _, skip := range ignoredErrors {
    if err == skip {
      // accepted by the linter. skip is treated as sentinel
      return nil
    }
    if skipErr := skip; err == skipErr {
      // LINT ERROR: Expected errors.Is but got ==
      return nil
    } else if errors.Is(err, skipErr) {
      // linter is happy again
      return nil
    }
  }
  return err
}

About

golang analysis for checking error types by equality instead of errors.Is

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages