Documentation ¶
Overview ¶
Package errors follows 2 goals:
1. Provide a simple way to enrich an error with some context details, using go13 Wrap/Unwrap mechanism. Such types of context are: - stacktrace - annotation - additional msg - error, which is suppressed by newer one - any value that can be accessed by corresponding key any time later
2. Provide a convenient way to handle errors in exception style.
About first point:
Error might look like a context vice versa. It also flows through the function call stack, but, opposite to context, it flows upwards. While it flows we want to enrich it with some information relevant to current stack level. This package provides several wrapping constructs providing this ability.
One can wrap an existing error with an error with a stacktrace, so that this error will be possible to print with it's stacktrace and easily find a place error occured in. Only the most deep stacktrace is saved and printed.
One can add an annotation to an error. Moreover, if error already has a stacktrace, this annotation will be printed along with the corresponding line in stacktrace, so it's easy to find in which function this annotations was added.
One can suppress an existing error with newer one. Both errors will be printed.
One can add a value to an error. Later this value can be retrieved with the key used to add this value. If several values are added with the same key, only the first one (from the most deep stack level) is saved. It might be very useful to pass logger to the function upper in the stack. Look at the example to see this approach.
About second point:
Go's error handling idiom usually looks like
x, err := DoSmth() if err != nil { logger.Error(err) // may be some err processing return err }
It is very "holy war" question which way to handle errors, exception like or "returning err" like, is better. Everyone chooses himself which way is more appropriate to him. But doubtlessly "returning err" style has some disadvantages.
1. Error handling logic is mixed with "business" logic so it shadows it
2. Code repetitions, which can cause a bug
3. Verbosity
That's why Go 2 draft https://go.googlesource.com/proposal/+/master/design/go2draft.md contains new Handle-Check error handling approach, which is quite similar to exception approach. This package provides similar to this Handle-Check mechanism, based on panics. Details are below =)
Example of error format, when printed with SprintE:
ERROR: task can not be marked as done STACK: github.com/pashaosipyants/errors/v2_test.markTaskAsDone /work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:105 github.com/pashaosipyants/errors/v2_test.apiCreateTask.func1 /work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:78 github.com/pashaosipyants/errors/v2.Handler /work/go/src/github.com/pashaosipyants/errors/handlecheck.go:55 github.com/pashaosipyants/errors/v2_test.apiCreateTask /work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:90 github.com/pashaosipyants/errors/v2_test.Example.func1 work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:60 github.com/pashaosipyants/errors/v2_test.Example /work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:63 testing.runExample /Go/src/testing/run_example.go:62 testing.runExamples /Go/src/testing/example.go:44 testing.(*M).Run /Go/src/testing/testing.go:1200 main.main _testmain.go:58 runtime.main /Go/src/runtime/proc.go:203 runtime.goexit /Go/src/runtime/asm_amd64.s:1373 SUPPRESSED: ERROR: User: 239; Error: already_exist_but_not_done STACK: github.com/pashaosipyants/errors/v2/example_auxiliary.SaveTaskToDbMockExistButNotDone /work/go/src/github.com/pashaosipyants/errors/example_auxiliary/example_auxiliary.go:36 github.com/pashaosipyants/errors/v2/example_auxiliary.CreateTaskInitedByUser /work/go/src/github.com/pashaosipyants/errors/example_auxiliary/example_auxiliary.go:78 ANNOTATION: Inited by user 239 github.com/pashaosipyants/errors/v2_test.apiCreateTask /work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:89 github.com/pashaosipyants/errors/v2_test.Example.func1 /work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:60 github.com/pashaosipyants/errors/v2_test.Example /work/go/src/github.com/pashaosipyants/errors/example_compehensive_test.go:63 testing.runExample /Go/src/testing/run_example.go:62 testing.runExamples /Go/src/testing/example.go:44 testing.(*M).Run /Go/src/testing/testing.go:1200 main.main _testmain.go:58 runtime.main /Go/src/runtime/proc.go:203 runtime.goexit /Go/src/runtime/asm_amd64.s:1373
Example ¶
This is comprehensive, pretending to be close to real-life example of using this package. It's easier to see it in code, but if you use godoc, please, notice https://godoc.org/github.com/pashaosipyants/errors/example_auxiliary package, which is used here.
Imagine there is an api to create a task that is executed by another service. Besides user of this api wants to hold info whether this task is already done. This api, ofc, can return error. E.g. certain task may be already created. If so, error should report whether it is done or not.
package main import ( "bytes" "fmt" "os" "github.com/sirupsen/logrus" . "github.com/pashaosipyants/errors/v2" "github.com/pashaosipyants/errors/v2/example_auxiliary" ) // This is comprehensive, pretending to be close to real-life example of using this package. // It's easier to see it in code, but if you use godoc, please, notice // https://godoc.org/github.com/pashaosipyants/errors/example_auxiliary package, // which is used here. // // Imagine there is an api to create a task that is executed by another service. // Besides user of this api wants to hold info whether this task is already done. // This api, ofc, can return error. E.g. certain task may be already created. // If so, error should report whether it is done or not. func main() { l := logrus.New().WithField("application", "tasks") l.Logger.SetFormatter(TerminalVerbose{}) l.Logger.SetOutput(os.Stdout) // loop to work out different cases for i := 0; i < 4; i++ { // func is smth like try block here func() { defer l.Infof("\n\tCase %d finished\n-------------------------------------------\n\n\n", i) // smth like catch block defer Handler(func(err error) { switch ValueE(err, "api") { case errcode_apicreatetaskfailed: logger, ok := ValueE(err, "logger").(*logrus.Entry) // logger with relevant fields of functions deeper in the call stack if !ok { logger = l } var ue *UserError if AsE(err, &ue) { logger = logger.WithField("user", ue.user) } logger.Error(SprintE(err)) // log // may be some specific actions case errcode_apiuserloginfailed: // may be some specific actions panic("Assertion failed") // but in our example can't be here default: panic("Assertion failed") } }) Check( apiUserLogin(l), OValue("api", errcode_apiuserloginfailed)) Check( apiCreateTask(l, i), OValue("api", errcode_apicreatetaskfailed)) l.Info("Success!!!\n") // log }() } } const errcode_apicreatetaskfailed = "api_create_task_failed" const errcode_apiuserloginfailed = "api_user_login_failed" func apiCreateTask(l *logrus.Entry, i int) (reterr error) { defer Handler(func(err error) { // do some specific logic - e.g. mark task in db as done switch { case IsE(err, example_auxiliary.ErrTaskAlreadyExistButNotDone): errOnMark := markTaskAsDone() reterr = AnyE( WrapE(errOnMark, OSupp(err)), err, ) default: reterr = err } // common logic }) err := example_auxiliary.CreateTaskInitedByUser(l, i, 239) Check(WrapUserError(err, 239)) return nil } // pretends to be always success func apiUserLogin(l *logrus.Entry) error { // some work return nil } // pretends that there is an error an task can not be marked as done func markTaskAsDone() (reterr error) { defer DefaultHandler(&reterr) CheckIf(true, Error("task can not be marked as done")) return } func WrapUserError(err error, user int) error { if err == nil { return nil } return &UserError{ user: user, err: err, } } type UserError struct { user int err error } func (x *UserError) Error() string { return x.err.Error() } func (x *UserError) Unwrap() error { return x.err } func (x *UserError) Format(f fmt.State, verb rune) { if verb == 's' { fmt.Fprint(f, x.err.Error()) } else { fmt.Fprintf(f, "User: %d; Error: %v", x.user, x.err) } } /////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////// type TerminalVerbose struct { } const breaker = "........................................................\n" func (t TerminalVerbose) Format(e *logrus.Entry) ([]byte, error) { if e.Level == logrus.InfoLevel { msg := lineBreaker(e.Message) return []byte(msg), nil } msg := lineBreaker(e.Message) var fields string for k, v := range e.Data { msg := fmt.Sprint(v) msg = lineBreaker(msg) fields += "--" + k + ":\n" + msg } var b *bytes.Buffer if e.Buffer != nil { b = e.Buffer } else { b = &bytes.Buffer{} } _, err := b.WriteString("\n" + breaker + msg + "\n" + fields) if err != nil { return nil, err } return b.Bytes(), nil } func lineBreaker(in string) string { if len(in) > 0 && in[len(in)-1] != '\n' { in += "\n" } return in }
Output: wrong output specially to make this function be executed and see output of this example
Index ¶
- Constants
- Variables
- func AnyE(errs ...error) error
- func AnyFuncE(errfuncs ...func() error) error
- func Check(err error, opts ...OptionE)
- func CheckIf(ifErr bool, err error, opts ...OptionE)
- func CheckIfNoStack(ifErr bool, err error, opts ...OptionE)
- func CheckNoStack(err error, opts ...OptionE)
- func DefaultHandler(reterr *error, elseDefer ...func())
- func Error(text string, opts ...OptionE) error
- func Errorf(format string, a ...interface{}) errf
- func Handler(handle func(err error), elseDefer ...func())
- func SprintE(err error) string
- func SuppressedE(err error) (supps []error)
- func ValueE(err error, key interface{}) interface{}
- func WrapAnnotationE(err error, annotation string, skip ...int) error
- func WrapE(err error, opts ...OptionE) error
- func WrapStackE(err error, skip ...int) error
- func WrapSuppressedE(err, suppressed error) error
- func WrapValueE(err error, key, value interface{}) error
- type OptionE
- type ToSkipE
Examples ¶
Constants ¶
const ErrStackMaxDepth = 32
Maximum depth of stack recorded to an error. One can change it, but be cautious. Set value only in initialization part of a program before any error is created not to cause concurrency problems.
Variables ¶
var AsE = errors.As
var IsE = errors.Is
var NewE = errors.New
var UnwrapE = errors.Unwrap
Functions ¶
func AnyFuncE ¶
AnyFuncE is a helper function that executes funcs one by one, stops if error occurred and returns it
func Check ¶
Check panics with an error. One can handle this panic defering Handler func. opts allows to add context to the error. OStack is enabled by default.
func CheckIf ¶
CheckIf panics with an error if ifErr is true. One can handle this panic defering Handler func. opts allows to add context to the error. OStack is enabled by default.
func CheckIfNoStack ¶
The same as CheckIf, but OStack is not enabled by default.
func CheckNoStack ¶
The same as Check, but OStack is not enabled by default.
func DefaultHandler ¶
func DefaultHandler(reterr *error, elseDefer ...func())
as Handler, but instead of running your handle function put arisen error in reterr.
func Errorf ¶
func Errorf(format string, a ...interface{}) errf
Creates new error. E.g.: Errorf("user with id %s is absent", userID).Opts(OStack(), OAnno(text))
func Handler ¶
func Handler(handle func(err error), elseDefer ...func())
defer Handler func and provide handler to process panics made by Check...(). Panics with type different to this package's one are just forwarded. elseDefer - deferred functions which will be executed if no error arisen. other panics are not intercepted. don't forget that Handler may not obtain error if u recover earlier.
func SprintE ¶
Returns detailed error description. With error msg, stacktrace, annotations, and suppressed errors.
func SuppressedE ¶
func ValueE ¶
func ValueE(err error, key interface{}) interface{}
Gets value by key saved in err. Returns nil if not found.
func WrapAnnotationE ¶
WrapAnnotationE returns error with err wrapped in and annotation(additional message) added. returnederr.Error() will be the same as err.Error(), but one can use SprintE(returnederr) to print it with annotations. If err is nil returns nil. UnwrapE(returnederr) == err. The sense of annotation is that by SprintE it's printed along with corresponding stacktrace level. So it's obvious in what place in code this annotation was added.
skip is optional param. First of variadic parameters is used, else are ignored. skip specifies the number of stacktrace levels to skip. By default stacktrace starts with the caller of WrapAnnotationE.
func WrapE ¶
WrapE - one function that does anything other Wrap... functions can. Instead of doing smth like WrapStackE(WrapAnnotationE(WrapValueE(err, key, value), "text")) just use several options in one WrapE. Like this: WrapE(err, OStack(), OAnno(text), OValue(key, value)).
func WrapStackE ¶
WrapStackE returns error with err wrapped in and stacktrace recorded. returnederr.Error() will be the same as err.Error(), but one can use SprintE(returnederr) to print it with stacktrace. If err is nil returns nil. UnwrapE(returnederr) == err.
skip is optional param. First of variadic parameters is used, else are ignored. skip specifies the number of stacktrace levels to skip. By default stacktrace starts with the caller of WrapStackE.
func WrapSuppressedE ¶
WrapSuppressedE returns error with err wrapped in and suppressed error added. returnederr.Error() will be the same as err.Error(), but one can use SprintE(returnederr) to print it with suppressed error. If err is nil returns nil. UnwrapE(returnederr) == err. List of suppressed errors can be retrieved with the SuppressedE.
func WrapValueE ¶
WrapValueE returns error with err wrapped in and value added. returnederr.Error() will be the same as err.Error(). If err is nil returns nil. UnwrapE(returnederr) == err. Value can be retrieved from returnederr(or any wrappers of it) by specified key with the ValueE function.
Types ¶
type ToSkipE ¶
type ToSkipE int
func (ToSkipE) WrapE ¶
The same as WrapE, but skip can be specified to skip several stacktrace levels. This is useful when WrapE is used inside some utility function which will be useless in stacktrace. E.g. see implementation of Error in this package. It utilizes WrapE, but skips itself from the stacktrace.