Lovin' your errors

How a little TLC pays back with interest.

Roger Peppe

Canonical Ltd

Most common phrase in Go?

if err != nil {
    return err
}

If Go had exceptions

func doSomething() {
    foo()
    bar()
    baz()
}

What we actually write

func doSomething() error {
    if err := foo(); err != nil {
        return err
    }
    if err := bar(); err != nil {
        return err
    }
    if err := baz(); err != nil {
        return err
    }
    return nil
}

Why is this better?

Robustness

Explicit makes readable

func doSomething() {
    foo()
    bar()
    baz()
}

Maintainability

Panic

Handle those errors!

Panic

// MustCompile is like Compile but panics if the expression cannot be parsed.
// It simplifies safe initialization of global variables holding compiled regular
// expressions.
func MustCompile(str string) *Regexp {
    regexp, error := Compile(str)
    if error != nil {
        panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
    }
    return regexp
}

Return

if err != nil {
    return err
}

Ignore

go get github.com/kisielk/errcheck
% errcheck ./...
agent/agent.go:654:11    buf.Write(data)
agent/bootstrap.go:96:12    st.Close()
agent/identity.go:29:12    os.Remove(c.SystemIdentityPath())
agent/tools/toolsdir.go:57:16    defer zr.Close()

Log

// IncCounterAsync increases by one the counter associated with the composed
// key. The action is done in the background using a separate goroutine.
func (s *Store) IncCounterAsync(key []string) {
    s.Go(func(s *Store) {
        if err := s.IncCounter(key); err != nil {
            logger.Errorf("cannot increase stats counter for key %v: %v", key, err)
        }
    })
}

Gather

// Errors holds any errors encountered during the parallel run.
type Errors []error

func (errs Errors) Error() string {
    switch len(errs) {
    case 0:
        return "no error"
    case 1:
        return errs[0].Error()
    }
    return fmt.Sprintf("%s (and %d more)", errs[0].Error(), len(errs)-1)
}

Diagnose

Diagnosis techniques 1 - special value

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

err := environments.Find(bson.D{{"_id", id}}).One(&env)
if err == mgo.ErrNotFound {
    return nil
}

Diagnosis techniques 2 - special type

type PathError struct {
        Op   string
        Path string
        Err  error
}

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

    err := os.Create("/tmp/something")
    if err, ok := err.(*os.PathError); ok {
    }

Diagnosis techniques 3 - interface

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

if err, ok := err.(net.Error); ok && err.Temporary() {
    ...
}

Diagnosis techniques 4 - function predicate

func IsNotExist(err error) bool {
    switch pe := err.(type) {
    case nil:
        return false
    case *PathError:
        err = pe.Err
    case *LinkError:
        err = pe.Err
    }
    return err == syscall.ENOENT || err == ErrNotExist
}

Why so many techniques?

Recap

Annotated error messages

if err != nil {
    return "", fmt.Errorf("cannot read random secret: %v", err)
}

Errors with concurrency

for {
    select {
    case msg := <-msgChan:
        err := doSomething(msg.params)
        msg.reply <- fmt.Errorf("cannot do something: %v", err)
    ...
    }
}

If everyone annotates, we get nice messages

"cannot read random secret: unexpected EOF"

If no-one annotates, error messages are less useful

cannot get login entity:
cannot get user "admin":
mongo query failed:
cannot acquire socket:
cannot log in:
error authorizing user "admin":
request failed:
cannot read reply:
EOF

But...!

err := fmt.Errorf("cannot open file: %v", err)
if os.IsNotExist(err) {
    // never reached
}

An augmented approach

Annotation in errgo

if err != nil {
    return errgo.Notef(err, "cannot read random secret")
}

Diagnosis in errgo

if errgo.Cause(err) == io.UnexpectedEOF {
    ...
}

if os.IsNotExist(errgo.Cause(err)) {
    ...
}

Cause is masked by default

Causes with errgo: simplest case

return errgo.Mask(err)

Causes with errgo: mask predicates

// Preserve any cause
return errgo.Mask(err, errgo.Any)

// Preserve only some causes
return errgo.Mask(err, os.IsNotExist, os.IsPermission)

Causes with errgo: annotate and mask

return errgo.NoteMask(err, "cannot open database file", os.IsNotExist)

Causes with errgo: choice of cause

if err == mgo.IsNotFound {
    return errgo.WithCausef(err, params.ErrNotFound, "no such document")
}

Error printing in errgo:

fmt.Printf("%v\n", err)

cannot encrypt password: cannot read random secret: unexpected EOF

fmt.Printf("%#v", err)

[{/home/rog/src/github.com/juju/utils/encrypt.go:97: cannot encrypt password}
{/home/rog/src/github.com/juju/utils/password.go:32: cannot read random secret}
{unexpected EOF}]

errgo data structure

Conclusion

Thank you

Roger Peppe

Canonical Ltd