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? Errors are important!

We spend lots of time looking at errors

A good error message can save days of work

Errors happen all the time Robustness Reasonable behaviour in small programs

... but not in large ones.

Failure is common Explicit makes readable Is this function robust under failure?

With exceptions, I have no idea

In Go: yes! func doSomething() { foo() bar() baz() } Maintainability Principle attributed to Jonathon Blow:

Making small logical changes should not require/result in wildly varying program code

Rather, changes should be as small as possible Panic Go actually does have exceptions

Reserved for truly exceptional situations

Never part of a public interface

Instead, consider each error individually Handle those errors! Panic

Return

Ignore

Log

Gather

Diagnose Panic When an error really should not happen

Broken internal invariants

Initialisation errors // 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 Most common option

Maligned but can actually add value (wait and see) if err != nil { return err } Ignore Generally frowned upon

Errors are usually returned for good reason

But sometimes it's OK (bytes.Buffer, log messages)

There's a tool that can can check for you 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 When you want to carry on regardless

Usually in high level logic

Need to choose a logging framework. // 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 Common in concurrent situations

Could just choose the first one

Or make an error that holds them all

... but that can end up unreadable. // 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 "Identify the nature of a problem by examination of the symptoms"

Actually relatively unusual Diagnosis techniques 1 - special value Define a specific error value.

Classical example: io.EOF

Useful when we don't need any more info

Very cheap 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 Define a special error type

Error is just an interface with a single method

Classical example: os.PathError 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 Extensible to multiple types/values 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 Allows arbitrary logic

Classical example: os.IsNotExist 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? Each one has its own trade-offs.

Error is always interpreted in context...

... because it comes from a single source

Properly modular ...

... except when many places just return the error Recap Errors indicate some failure

Influence control flow

Are used in context

Also used to describe errors Annotated error messages Like a backwards stack

Usually added by return statements

Works well with concurrency 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 Message reads like a story

cannot read random secret because of unexpected EOF "cannot read random secret: unexpected EOF" If no-one annotates, error messages are less useful True story

Click on Login, got reply "EOF"

Error actually traversed 13 stack levels

Could have been 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...! Annotation obscures diagnosis

Either return custom value/type

... or hide the diagnosis entirely err := fmt.Errorf("cannot open file: %v", err) if os.IsNotExist(err) { // never reached } An augmented approach gopkg.in/errgo.v1

Cause-preserving annotation

Records source location

Agnostic about actual errors

Can use all the existing techniques Annotation in errgo Similar to fmt.Errorf

Also records source location if err != nil { return errgo.Notef(err, "cannot read random secret") } Diagnosis in errgo Cause function works on any error

Returns underlying cause, ignoring annotations

Default cause is error itself if errgo.Cause(err) == io.UnexpectedEOF { ... } if os.IsNotExist(errgo.Cause(err)) { ... } Cause is masked by default Constrains possible causes

Error causes are part of API

Explicit is much better for maintenance Causes with errgo: simplest case Record the source location

Mask the cause return errgo.Mask(err) Causes with errgo: mask predicates Record source location

Preserve selected causes

Predicate function allows specific causes // 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 Record source location

Add annotation

Preserve selected causes return errgo.NoteMask(err, "cannot open database file", os.IsNotExist) Causes with errgo: choice of cause Choose another cause

Add annotation if err == mgo.IsNotFound { return errgo.WithCausef(err, params.ErrNotFound, "no such document") } Error printing in errgo: fmt.Printf("%v

", 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 If you pay attention to your errors, you will be rewarded.

Existing practice leaves something to be desired.

It's possible to do better and stay compatible with existing practice. Thank you Roger Peppe Canonical Ltd rogpeppe@gmail.com @rogpeppe

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.

(Press 'H' or navigate to hide this message.)