Errors in Go - let's do it

right

source: https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html

by Rob Pike

Errors in Go - let's do it

right

...in some cases

Agenda

  • Panic like a Gopher

  • Error context
  • Error Strategies
    • Sentinel errors

    • Typed errors

    • Behaviour errors

Panic like a Gopher

Panic like a Gopher

Panic like a Gopher

Panic like a Gopher

Gophers does not panic!

GOPHERS HANDLES

ERRORS GRACEFULLY

Errors - Go approach

func (c *Client) Get(url string) (resp *Response, err error)

It is idiomatic in Go to use the error interface type as the return type for any error that is going to be returned from a function or method.

Errors - Go approach

  • Deal with errors first!
  • Multiple return statements
  • Keep the normal code path at a minimal indentation

Panic function - pattern

(..)Errors are just values and can be programmed in different ways to suit different situations"

Idomatic GO

func (c *Client) Get(url string) (resp *Response, err error)

Return Error Strategies

Sentinel errors

//http://golang.org/src/pkg/bufio/bufio.go
var (
   ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
   ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
   ErrBufferFull        = errors.New("bufio: buffer full")
   ErrNegativeCount     = errors.New("bufio: negative count")
)

Sentinel errors

func (m *meteredExpirator) collectMetric() {
    // ...
    err := m.underlying.Expire(ctx, maxTime)
    switch err {
    case nil:
        m.expirationSuccessRemoved.Inc(1)
    case ErrNothingToExpire:
        m.expirationSuccessEmpty.Inc(1)
    default:
         m.expirationFailed.Inc(1)
    }
    // ...
}

Props and cons of using sentinel errors

props

  • fast & simple do define

cons

  • error value  becomes a part of your public API
  • cannot add more context to error in feature
  • creates dependencies between packages

Error types

// https://golang.org/pkg/os/#PathError


// PathError records an error and the operation and file path that caused it.
type PathError struct {
    Op   string
    Path string
    Err  error
}



func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

 

Sometimes the caller needs extra context in order to make a more informed error handling decision.

Error types



err := something()

switch e := err.(type) {
case nil:
    // success
case *os.PathError:
    log.Printf("while executing something, got err: %v", e)
    if e.Op == "mkdir" {
        // drop custom metric
    } else {
        // drop custom metric
    }
default:
    // unknown error
}

 

Props of using error types

props

  • more context about an error
  • easy to extend in feature without breaking API

Assert errors for behaviour, not type

Interactions with the world outside your process, like network activity, require that the caller investigate the nature of the error to decide if it is reasonable to retry the operation."

Assert errors for behaviour, not type

// DNSError represents a DNS lookup error.
type DNSError struct {
    Err         string // description of the error
    Name        string // name looked for
    Server      string // server used
    IsTimeout   bool   // if true, timed out; not all timeouts set this
    IsTemporary bool   // if true, error is temporary; not all errors set this
}

func (e *DNSError) Error() string {
// ...
}

// Temporary reports whether the DNS error is known to be temporary.
// This is not always known; a DNS lookup may fail due to a temporary
// error and return a DNSError for which Temporary returns false.
func (e *DNSError) Temporary() bool { return e.IsTimeout || e.IsTemporary }

 

Assert errors for behaviour, not type

err := c.processItem(key.(string))

switch {
case err == nil:
    c.queue.Forget(key)

case IsTemporaryError(err):
    c.log.Errorf("Error processing %q (will retry): %v", key, err)
    c.queue.AddRateLimited(key)

default:
    c.log.Errorf("Error processing %q (giving up): %v", key, err)
    c.queue.Forget(key)
}

 

Assert errors for behaviour, not type



// IsTemporaryError returns true if error implements following interface:
//  type temporary interface {
//      Temporary() bool
//  }
//
// and Temporary() method return true. Otherwise false will be returned.
func IsTemporaryError(err error) bool {
    type temporary interface {
        Temporary() bool
    }

    te, ok := err.(temporary)
    return ok && te.Temporary()
}

 

Assert errors for behaviour, not type

err := svc.instanceInserter.Insert(...)

switch {
case err == nil:
    // 202 Accepted
case IsActiveOperationInProgressError(err):
    // provisioning in progress: 202 Accepted
case IsAlreadyExistsError(err):
    // all filed are the same: 200 OK
    // same ID different fields: 409 Conflict
default:
    // 400 Bad Request
    return fmt.Errorf("cannot schedule instance to provision: %s", err.Error())
}

 

Assert errors for behaviour, not type

// IsNotFoundError checks if given error is NotFound error
func IsNotFoundError(err error) bool {
    nfe, ok := err.(interface {
        NotFound() bool
    })

    return ok && nfe.NotFound()
}


// IsAlreadyExistsError checks if given error is AlreadyExist error
func IsAlreadyExistsError(err error) bool {
    aee, ok := err.(interface {
        AlreadyExists() bool
    })

    return ok && aee.AlreadyExists()
}

 

Error Context

Error Context

func (c *Controller) processItem(key string) error {
    obj, exists, err := c.informer.Informer().GetIndexer().GetByKey(key)
    if err != nil {
        return err
    }

    if !exists {
        if err := c.reRemover.Remove(internal.RemoteEnvironmentName(key)); err != nil {
            return err
        }
        return nil
    }

    replaced, err := c.reUpserter.Upsert(dm)
    if err != nil {
        return err
    }

    return nil
}
if err := processItem(key); err != nil {
    log.Errorf("Error processing %q: %v", key, err)
    return
}

 

{
  "level": "error",
  "log": {
     "message": "Error processing 'ns/re1': no 
                 reachable servers",
  }
}

Error Context

// https://github.com/pkg/errors



err := c.reRemover.Remove(internal.RemoteEnvironmentName(key))
if err != nil {
  return errors.Wrapf(err, "while removing RE with name %q from storage", key)
}

Error Context

{
  "level": "error",
  "log": {
     "message": "Error processing 'ns/re1': while removing remote environment  
                 with name 're1' from storage: while connecting to database: no
                 reachable server",
  }
}

Error Context

Q&A

Thank you