package try /* try is a fork of lainio's err2 package Copyright (c) 2019 Lainio, MIT License https://github.com/lainio/err2 */ import ( "errors" "fmt" "os" "regexp" "runtime" "strings" ) // Err is an interface for an error with extended stack info. type Err interface { Error() string Unwrap() error CallStack() []call CallStackString() string PrintCallStack() GetData() *tryErrData Annotate(msg string) } // tryErr is an extended error struct. type tryErr struct { err error annotations []string callStack []call } // tryErrData is exported error data. Can be converted to JSON. type tryErrData struct { Msg string `json:"msg"` Annotations []string `json:"annotations"` CallStack []call `json:"call_stack"` } type call struct { File string `json:"file"` Function string `json:"fn"` Line int `json:"l"` } // FromErr wraps a standard error into a try-compatible one with extended stack info. func FromErr(err error) Err { // If the error is already try-compatible, return //nolint:errorlint cterr, ok := err.(Err) if ok { return cterr } terr := &tryErr{err: err, callStack: []call{}} pc, _, _, ok := runtime.Caller(0) if !ok { return terr } thisFnc := runtime.FuncForPC(pc) if thisFnc == nil { return terr } rexp := regexp.MustCompile(`.+\/`) thisPkg := rexp.FindString(thisFnc.Name()) for i := 1; ; i++ { pc, f, l, ok := runtime.Caller(i) if !ok { break } fnc := runtime.FuncForPC(pc).Name() if strings.HasPrefix(fnc, thisPkg) && !strings.HasSuffix(f, "_test.go") { continue } terr.callStack = append(terr.callStack, call{File: f, Function: fnc, Line: l}) } return terr } // NewErr creates a new try-compatible error with extended stack info. func NewErr(msg string) Err { return FromErr(errors.New(msg)) } // Error returns the standard go error message (initial message + annotations). func (e *tryErr) Error() string { msgs := e.annotations msgs = append(msgs, e.err.Error()) return strings.Join(msgs, ": ") } // Unwrap returns the standard go error. func (e *tryErr) Unwrap() error { return e.err } // CallStack returns a slice of function calls leading to the error, // with the most recent being first. func (e *tryErr) CallStack() []call { return e.callStack } // CallStackString outputs the call stack as a printable multiline string. func (e *tryErr) CallStackString() string { res := fmt.Sprintf("ERROR: %s\n", e.err.Error()) for _, msg := range e.annotations { res += fmt.Sprintln(msg) } res += fmt.Sprintln("\nTraceback (most recent call first):") for _, c := range e.callStack { res += fmt.Sprintf("%s:%d\n", c.File, c.Line) res += fmt.Sprintf(" %s\n", c.Function) } return res } // PrintCallStack prints the call stack to stderr. func (e *tryErr) PrintCallStack() { _, _ = fmt.Fprintln(os.Stderr, e.CallStackString()) } // GetData returns a serializable error data object. func (e *tryErr) GetData() *tryErrData { return &tryErrData{ Msg: e.err.Error(), CallStack: e.callStack, Annotations: e.annotations, } } // Annotate adds additional text to the error message. func (e *tryErr) Annotate(msg string) { e.annotations = append(e.annotations, msg) } // Empty is a helper method to handle errors of func() (string, error) functions. func Empty(_ interface{}, err error) { Check(err) } // Any is as similar as proposed Go2 Try macro, but it's a function and it // returns slice of interfaces. It has quite big performance penalty when // compared to Check function. func Any(args ...interface{}) []interface{} { check(args) return args } // Check performs the error check for the given argument. If the err is nil, // it does nothing. According the measurements, it's as fast as if err != nil // {return err} on happy path. func Check(err error) { if err != nil { panic(FromErr(err)) } } // Checks the error status of the last argument. It panics with "wrong // signature" if the last calling parameter is not error. In case of error it // delivers it by panicking. func check(args []interface{}) { argCount := len(args) last := argCount - 1 if args[last] != nil { err, ok := args[last].(error) if !ok { panic("wrong signature") } panic(FromErr(err)) } } // Handle is for adding an error handler to a function by defer. It's for // functions returning errors them self. For those functions that doesn't // return errors there is a Catch function. Note! The handler function f is // called only when err != nil. func Handle(err *Err, f func()) { // We put real panic objects back and keep only those which are // carrying our errors. We must also call all of the handlers in defer // stack. switch r := recover(); r.(type) { case nil: // Defers are in the stack and the first from the stack gets the // opportunity to get panic object's error (below). We still must // call handler functions to the rest of the handlers if there is // an error. if *err != nil { f() } case Err: // We or someone did transport this error thru panic. *err = r.(Err) f() default: panic(r) } } // Catch is a convenient helper to those functions that doesn't return errors. // Go's main function is a good example. Note! There can be only one deferred // Catch function per non error returning function. See Handle for more // information. func Catch(f func(err Err)) { if r := recover(); r != nil { e, ok := r.(Err) if !ok { panic(r) } f(e) } } // CatchAll is a helper function to catch and write handlers for all errors and // all panics thrown in the current go routine. func CatchAll(errorHandler func(err Err), panicHandler func(v interface{})) { if r := recover(); r != nil { e, ok := r.(Err) if ok { errorHandler(e) } else { panicHandler(r) } } } // CatchTrace catches all errors and prints their call stack. // Setting the exit parameter to a value above 0 will make the program // exit in case of an error. func CatchTrace(exit int) { if r := recover(); r != nil { e, ok := r.(Err) if !ok { panic(r) } e.PrintCallStack() if exit > 0 { os.Exit(exit) } } } // Return is same as Handle but it's for functions which don't wrap or annotate // their errors. If you want to annotate errors see Annotate for more // information. func Return(err *Err) { if r := recover(); r != nil { e, ok := r.(Err) if !ok { panic(r) // Not ours, carry on panicking } *err = e } } // ReturnStd is like Return, but it returns the Go standard error type. func ReturnStd(err *error) { if r := recover(); r != nil { e, ok := r.(Err) if !ok { panic(r) // Not ours, carry on panicking } *err = e.Unwrap() } } // Annotate adds additional messages to the error. func Annotate(err *Err, msg string) { if r := recover(); r != nil { e, ok := r.(Err) if !ok { panic(r) // Not ours, carry on panicking } *err = e e.Annotate(msg) } else if *err != nil { // if other handlers call recovery() we still.. (*err).Annotate(msg) } }