name: inverse class: title, inverse Go2 Error values ## anarcher@gmail.com ## 2019-04-25 .footnote[ ] --- # Intro - This slide is about the improvements to the error **inspection** and **printing**. - https://golang.org/design/29934-error-values - The `error inspection & printing` doesn't change the language ifself (almost) - https://github.com/golang/xerrors/ - This package will be incorporated into the standard library's errors package in Go **1.13** --- # Background Four ways to test errors -- - Sentinel errors (value equality) `err == io.ErrUnexpectedEOF` - Error structs/types (type assertion or type switch) `pe, ok := err.(*os.PathError)` - Error-checking predicate function `os.IsNotExit(err)` - String search `strings.Contains(err.Error(), "wtf")` --- # Sentinel errors ```go var ErrUnexpectedEOF = errors.New("unexpected EOF") ``` Errors are values. We can test for equality with sentinel errors like `io.EOF` ```go if err == io.ErrUnexpectedEOF { ... } ``` To provide more information about this error, We can define a new type implements the `error` interface. (Error types) --- # Error types ```go type PathError struct { Op string Path string Err error // wrapping , the cause } ``` ```go if pe,ok := err.(*os.PathError); ok { ... pe.Path ... } ``` `os.PathError` is a struct that includes a pathname. Programs can extract information from this error by using type assertion. --- # Error-checking predicate function ```go // IsExist returns a boolean indicating whether the error is known to report // that a file or directory already exists. It is satisfied by ErrExist as // well as some syscall errors. func IsExist(err error) bool { return isExist(err) } // IsNotExist returns a boolean indicating whether the error is known to // report that a file or directory does not exist. It is satisfied by // ErrNotExist as well as some syscall errors. func IsNotExist(err error) bool { return isNotExist(err) } ``` `os.IsNotExist` check for a specific kind of error, doing limited unwrapping --- # Unnamed error values ```go if err != nil { return fmt.Errorf("write err: %v",err) } strings.Contains(err.Error(), "wtf") ``` If callers need to test for specific errors, the callers should do substring searches in this error text (`err.Error()`) > IMHO, It wouldn't better than first third approaches --- # Go2: Error Inspection Go1 only supports string message of errors (`func (e error) Error() string`) Go2's goal is making errors more informative for both __programs and people__. __Inspecting errors__ - __Wrapping errors__ (chain of errors) is a idea to provide additional information of error. - `github.com/pkg/errors` - IMHO,it's some kind of composition. __Formatting errors__ helps more readable information of errors for **people** __Stack Frame__ is for location information (file name, line number) of errors --- # Go2: Error Inspection ```go write users database: more detail here mypkg/db.Open /path/to/database.go:111 - call myserver.Method: google.golang.org/grpc.Invoke /path/to/grpc.go:222 - dial myserver:3333: net.Dial /path/to/net/dial.go:333 - open /etc/resolv.conf: os.Open /path/to/os/open.go:444 - permission denied ``` --- # Inspecting errors ```go write users database: call myserver.Method: \ dial myserver:3333: open /etc/resolv.conf: permission denied ``` - `WriteError` provides "write users database", wraps `RPCError` - `RPCError` provides "call myserver.Method", wraps `net.OpError` - `net.OpError` provides "dial myserver:333", wraps `os.PathError` - `os.PathError` provides "open /etc/resolv.conf", wraps `syscall.EPERM` - `syscall.EPERM` provides "permission denied" For inspecting (chaining errors), Go2 supports `Is` and `As` functions. --- # `Wrapper` interface ```go type Wrapper interface { // Unwrap returns the next error in the error chain. // If there is no next error, Unwrap returns nil. Unwrap() error } ``` An error that wraps another error should implement `Wrapper` by defining an `Unwrap` method ```go // Unwrap returns the result of calling the Unwrap method on err, if err implements Unwrap. // Otherwise, Unwrap returns nil. func Unwrap(err error) error ``` --- # `Is` function ```go // Is reports whether any error in err's chain matches target. // // An error is considered to match a target if it is equal to that target or if // it implements a method Is(error) bool such that Is(target) returns true. func Is(err, target error) bool ``` The `Is` is equivalent of `if err == ErrSomething `, but it checks the chain of errors. The `Is` follows the chain of errors by calling `Unwrap`. It is intented to be used insteads of equality for matching __sentinel errors__. An error type can implement `func Is(error) bool` to override default behavior. --- # `As` function ```go // As finds the first error in err's chain that matches the type to which target // points, and if so, sets the target to its value and returns true. An error // matches a type if it is assignable to the target type, or if it has a method // As(interface{}) bool such that As(target) returns true. As will panic if target // is not a non-nil pointer to a type which implements error or is of interface type. // // The As method should set the target to its value and return true if err // matches the type to which target points. func As(err error, target interface{}) bool ``` The `As` function is equivalent of the `err, ok := err.(ErrSomething)`,but it checks the chain of errors. It's intented to be used insteads of type assertion or type switch for the __Error type__. An error type can implement `func As(target interface{}) bool` to override default behavior. --- # `As` function ```go type Temporary interface { Temporary() bool } ``` ```go fe := &FileError{err: FileNotFound} // has Temporary() method e := &ConfigError{err: we} var temp Temporary if xerrors.As(e, &temp) == false { t.Fatalf("not found: %v", e) } t.Logf("temp.Temporary: %v", temp.Temporary()) if xerrors.Is(e, FileNotFound) { t.Logf("err has FileNotFound") } ``` --- # Opaque ```go // Opaque returns an error with the same error formatting as err // but that does not match err and cannot be unwrapped. func Opaque(err error) error ``` The Opaque function hides a wrapped error from programmatic inspection. --- # Printing errors Formatter can be implemented by errors. Printer designed to allow `fmt` or i18n or customs. ```go type Formatter interface { error // FormatError prints the receiver's first error and returns the next error to be formatted, if any. FormatError(p Printer) (next error) } ``` ```go type Printer interface { // Print appends args to the message output. Print(args ...interface{}) // Printf writes a formatted string. Printf(format string, args ...interface{}) // Detail reports whether error detail is requested. Detail() bool } ``` --- # Formatting ```go func (e WriteError) FormatError(p xerrors.Printer) error { p.Print(e.Error()) if p.Detail() { e.frame.Format(p) } return e.Unwrap() } ``` ```go // go1.12 func (e WriteError) Format(f fmt.State, c rune) { // implements fmt.Formatter xerrors.FormatError(e, f, c) // will call e.FormatError } ``` ```go fmt.Printf("Error: %+v",err) ``` --- # Formatting Use `%+v` to print error in detailed format (multi-line) ```go write users database: mypkg/db.Open /path/to/database.go:111 - open /etc/resolv.conf: os.Open /path/to/os/open.go:444 ``` Use `%w` if you want to expose the underlying error to your callers. ```go //go1.13 e := fmt.Errorf("more info: %w", err) // err is sql.ErrTxDone if errors.Is(e,sql.ErrTxDone) { ... ``` - Works `FormatError` and `Unwrap` methods if the last argument is an error err and the format string ends with `: %s`, `: %v`, or `: %w` --- # Stack Frame The Frame type holds location information: the function name, file and line of a single stack frame. ```go type Frame struct { } // Format prints the stack as error detail. func (f Frame) Format(p Printer) { ... } func Caller(skip int) Frame { ... } ``` ```go func (e WriteError) FormatError(p errors.Printer) error { p.Print(e.Error()) if p.Detail() { e.frame.Format(p) } return e.Unwrap() } ``` --- # Who consumes our errors? __Programs__ - Programs use `Code` and `Details` to recover or do something else. - Can provide `Code` by SentinelError? (`io.EOF`) - Can provide `Details` by ErrorStruct? (`os.PathError`) __Users__ - For end users, Error should provide a __human-readable message__(`Your request is not accepted`) - `interface { Error() string }` __Operators__ - The operators need to the details of the system and works with this error. - They want to see as much information as possible.(*Code,Details,Message,StackFrame,...*) ```go *os.PathError -> *syscall.EPERM ``` --- # Example: Simple Error System It's inspired by [Error handling in Upspin](https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html) and uses go2 error value features. --- # Error type ```go // Error defines a application error type Error struct { // Kind is the kind of this error Kind Kind `json:"kind"` // Human-readable message Message string `json:"message"` // Logical operation. usually the name of method. Op Op `json:"op"` // Nested error Err error `json:"-"` frame errors.Frame } ``` --- # Error type ```go func (e *Error) Unwrap() error { return e.Err } ``` ```go func (e *Error) FormatError(p Printer) error { p.Print(e.Error()) if p.Detail() { p.Print(e.Message) } e.frame.Format(p) return e.Unwrap() } ``` ```go // +build !go1.13 func (e *Error) Format(f fmt.State, c rune) { // implements fmt.Formatter xerrors.FormatError(e, f, c) // will call e.FormatError } ``` --- # Error type ```go func (e *Error) Error() string { var b strings.Builder if e.Kind != "" { fmt.Fprintf(&b, "[%s]", e.Kind) } if e.Op != "" { if e.Kind != "" { b.WriteString(": ") } fmt.Fprintf(&b, "%s", e.Op) } return b.String() } ``` --- class: pic # Kind? Code?  https://http.cat/301 --- # Kind? Code? ```go // Kind defines the kind of error type Kind string // or uint8 ``` - https://godoc.org/upspin.io/errors#Kind ```go Other Kind = iota // Unclassified error. This value is not printed in the error message. Invalid // Invalid operation for this type of item. Permission // Permission denied. IO // External I/O error such as network failure. Exist // Item already exists. NotExist // Item does not exist. ``` - https://cloud.google.com/apis/design/errors - For example, instead of defining different kinds of "not found" errors, the server uses one standard `google.rpc.Code.NOT_FOUND` error code and tells the client which specific resource was not found. - Individual APIs should avoid defining additional error codes, since developers are very unlikely to write logic to handle a large number of error codes. --- # Op`eration` ```go // Op describes an operation type Op string ``` - The Op field denotes the operation being performed. - For example: `client.Lookup` , `dir/server.Glob` - Logical trace point for operators (debugging). ```go const serverDelete errors.Op = "server/delete" ``` --- # `E` function ```go func E(args ...interface{}) error { if len(args) == 0 { panic("call to errors.E with no arguments") } e := &Error{} for _, arg := range args { switch arg := arg.(type) { case Kind: e.Kind = arg case Op: e.Op = arg case string: e.Message = arg case error: e.Err = arg } } e.frame = errors.Caller(1) return e } ``` --- # `E` function ```go const ( ENOTFOUND = Kind("notfound") EINVALID = Kind("invalid") ) const ( DBDelOp Op = "db.delete" ServerDelOp Op = "server.delete" ) e := E(DBDelOp, ENOTFOUND, "this is error!") // Op, Kind, Message e2 := E(ServerDelOp, e) // Op, error ``` --- # `IsKind` ```go // Is reports that err.Kind and kind are equal or not. func IsKind(kind Kind, err error) bool { e, ok := err.(*Error) if ok { if e.Kind == kind { return true } } if err := errors.Unwrap(err); err != nil { return IsKind(kind, err) } return false } ``` ```go if IsKind(e2, ENOTFOUND) == false { t.Fatalf("want: %v have: %v", true, false) } ``` --- # `BestError` ```go // BestError returns a new error with latest Kind and Message of the chain of error func BestError(err error, args ...interface{}) *Error { var e *Error if len(args) <= 0 { e = &Error{} } else { e = E(args...).(*Error) } if e.Kind == "" { if err := ErrorKind(err); err != nil { e.Kind = err.Kind } } if e.Message == "" { if err := ErrorMessage(err); err != nil { e.Message = err.Message } } ``` --- # `BestError` ```go const notFound Kind = "notFound" const found Kind = "found" e1 := E(notFound, "message1") e2 := E(dbOp,e1) e3 := E(found,e2) e4 := E(serverOp, e3) bestErr := BestError(e4) if bestErr.Kind != found { t.Fatalf("kind: have: %v want: %v", bestErr.Kind, found) } if bestErr.Message != "message1" { t.Fatalf("message: have: %v want: %v", bestErr.Message, "message1") } json.Marshal(e4) // Output: { "op": "server", "kind" : "found", "message" : ... ``` --- # Error details Use wrapping error detail structs ```go *Error(Op, Kind(NotFound)) -> *PathError -> *Error(Op) -> io.EOF ``` ```go var p *os.PathError if IsKind(err,NotFound) && errors.As(err,&p) { // handle this. } ``` --- name: inverse class: title, inverse Thank you