From 4e5775fcfabc19fe37a3d774a2c529b6ae544c5c Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Wed, 9 Sep 2020 11:48:46 +0200 Subject: [PATCH 01/11] initial commit --- calltrace.go | 46 ++++++++++++++++++++ doc.go | 34 +++++++++++++++ error.go | 104 +++++++++++++++++++++++++++++++++++++++++++++ error_test.go | 17 ++++++++ error_tmpl.go | 35 +++++++++++++++ error_tmpl_test.go | 19 +++++++++ globals.go | 9 ++++ utils.go | 28 ++++++++++++ 8 files changed, 292 insertions(+) create mode 100644 calltrace.go create mode 100644 doc.go create mode 100644 error.go create mode 100644 error_test.go create mode 100644 error_tmpl.go create mode 100644 error_tmpl_test.go create mode 100644 globals.go create mode 100644 utils.go diff --git a/calltrace.go b/calltrace.go new file mode 100644 index 0000000..4a99722 --- /dev/null +++ b/calltrace.go @@ -0,0 +1,46 @@ +package adverr + +import ( + "runtime" + "strconv" + "strings" +) + +// CallTrace represents a call stack trace similar to Java's stack trace +type CallTrace struct { + frames *runtime.Frames +} + +// Trace returns a new CallTrace starting from this call +// Use skip to skip the first entries in the trace +func Trace(skip int) *CallTrace { + if !TraceCallStack { + return nil + } + + pc := make([]uintptr, CallStackLength) + n := runtime.Callers(skip+1, pc) + pc = pc[:n] + return &CallTrace{runtime.CallersFrames(pc)} +} + +func (ct *CallTrace) String() string { + if ct == nil { + return "" + } + + b := new(strings.Builder) + + for frame, ok := ct.frames.Next(); ok; frame, ok = ct.frames.Next() { + b.WriteString("\tat ") + b.WriteString(frame.Function) + b.WriteString(" (") + b.WriteString(frame.File) + b.WriteString(":") + b.WriteString(strconv.Itoa(frame.Line)) + b.WriteString(")") + b.WriteString("\n") + } + + return b.String() +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..bf988d4 --- /dev/null +++ b/doc.go @@ -0,0 +1,34 @@ +/* + Package adverr implements errors with call stack traces + as well as error templates for error equality + + Usage examples + + Creating templates: + var ( + ErrDoStuffFailed = adverr.NewErrTmpl("ErrDoStuffFailed", "Could'nt do stuff because of %s") + ) + + Creating independent error (without error template): + func doStuffWithIndependentErr() error { + return adverr.New("Could'nt do stuff") + } + + Creating error based on template: + func doStuff() error { + return ErrDoStuffFailed.New("reasons") + } + + Printing errors on stderr convieniently: + Print(myErr) + Println(myErr) + + Printing errors on stderr and exit with exitcode: + Fatal(myErr, 1) + Fatalln(myErr, 1) + + Advantages of error templates + two errors made by the same template will return true when called with errors.Is() + +*/ +package adverr diff --git a/error.go b/error.go new file mode 100644 index 0000000..bc99bae --- /dev/null +++ b/error.go @@ -0,0 +1,104 @@ +package adverr + +import ( + "errors" + "reflect" + "strings" +) + +// Error is a wrapper for error with stack trace +type Error struct { + msg string + callTrace *CallTrace + tmpl *ErrTmpl + cause error +} + +// New returns a new Error with the given message +func New(msg string) *Error { + return &Error{ + msg: msg, + cause: nil, + tmpl: nil, + callTrace: Trace(2), + } +} + +// Wrap returns a new Error with the given message which is caused by cause +func Wrap(msg string, cause error) *Error { + return &Error{ + msg: msg, + cause: cause, + callTrace: Trace(2), + } +} + +func errtype(err error) string { + if e, ok := err.(*Error); ok && e.tmpl != nil { + return errtype(e.tmpl) + } else if tmpl, ok := err.(*ErrTmpl); ok { + t := reflect.TypeOf(tmpl) + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + return t.PkgPath() + "." + tmpl.name + } + + t := reflect.TypeOf(err) + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + return t.PkgPath() + "." + t.Name() +} + +func (e *Error) Unwrap() error { + return e.cause +} + +func (e *Error) Error() string { + b := new(strings.Builder) + printErr(e, b) + return b.String() +} + +// Is implements the error equality function used by errors.Is() +// It returns true if the error is the same instance or is created using the same ErrTmpl +func (e *Error) Is(target error) bool { + // same error instance + if target == e { + return true + } + + // no template used, therefore no equality possible + if e.tmpl == nil { + return false + } + + // same template, therefore errors are equal to another + if tErr, ok := target.(*Error); ok { + return tErr.tmpl == e.tmpl + } + + return false +} + +func printErr(err error, b *strings.Builder) { + if e, ok := err.(*Error); ok { + b.WriteString(errtype(e)) + b.WriteString(": ") + b.WriteString(e.msg) + b.WriteString("\n") + b.WriteString(e.callTrace.String()) + } else { + b.WriteString(errtype(err)) + b.WriteString(": ") + b.WriteString(err.Error()) + b.WriteString("\n") + } + + cause := errors.Unwrap(err) + if cause != nil { + b.WriteString("Caused by ") + printErr(cause, b) + } +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..2c9b384 --- /dev/null +++ b/error_test.go @@ -0,0 +1,17 @@ +package adverr + +import ( + "errors" + "fmt" + "testing" +) + +func TestErr(t *testing.T) { + TraceCallStack = false + err := doStuff() + fmt.Println(err) +} + +func doStuff() error { + return Wrap("wrapped error", Wrap("test error", fmt.Errorf("asd: %w", errors.New("test")))) +} diff --git a/error_tmpl.go b/error_tmpl.go new file mode 100644 index 0000000..11e088c --- /dev/null +++ b/error_tmpl.go @@ -0,0 +1,35 @@ +package adverr + +import "fmt" + +var ( + // ErrTmplUsedAsErr is returned from ErrTmpl.Error() because an error template should never be used as an actual error value + ErrTmplUsedAsErr = NewErrTmpl("ErrTmplUsedAsErr", "Error template used as error value: %s") +) + +// ErrTmpl is an error template to define equalities between different errors +type ErrTmpl struct { + name string + format string +} + +// NewErrTmpl returns a new error template with the given format string as a predefined error message +func NewErrTmpl(name, format string) *ErrTmpl { + return &ErrTmpl{name, format} +} + +// Error implementation just for satisfying the error interface +// Please dont use ErrTmpls as actual errors +func (t *ErrTmpl) Error() string { + return ErrTmplUsedAsErr.New(errtype(t)).Error() +} + +// New returns a new Error in which the given values are being formatted into the format string of its template +func (t *ErrTmpl) New(args ...interface{}) *Error { + return &Error{ + msg: fmt.Sprintf(t.format, args...), + cause: nil, + tmpl: t, + callTrace: Trace(2), + } +} diff --git a/error_tmpl_test.go b/error_tmpl_test.go new file mode 100644 index 0000000..32629f5 --- /dev/null +++ b/error_tmpl_test.go @@ -0,0 +1,19 @@ +package adverr + +import ( + "fmt" + "testing" +) + +var ( + ErrDoStuffFailed = NewErrTmpl("ErrDoStuffFailed", "test error: %s") +) + +func TestErrTmpl(t *testing.T) { + err := doTemplateStuff() + fmt.Println(err) +} + +func doTemplateStuff() error { + return ErrDoStuffFailed.New("because of reasons") +} diff --git a/globals.go b/globals.go new file mode 100644 index 0000000..4c6de23 --- /dev/null +++ b/globals.go @@ -0,0 +1,9 @@ +package adverr + +// TraceCallStack decides if any call stack traces will be gathered when creating Errors +// If your application is doing performance-heavy tasks with lots of Error creations, you may consider setting this to false +// If set to false, all CallTraces in Error will be nil +var TraceCallStack bool = true + +// CallStackLength decides how many calls from the call stack should be gathered at most +var CallStackLength int = 100 diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..dba9e69 --- /dev/null +++ b/utils.go @@ -0,0 +1,28 @@ +package adverr + +import ( + "fmt" + "os" +) + +// Println prints the given err to stderr followed by a newline +func Println(err error) { + fmt.Fprintln(os.Stderr, err) +} + +// Print prints the given err to stderr +func Print(err error) { + fmt.Fprint(os.Stderr, err) +} + +// Fatalln prints the given err to stderr followed by a newline and exits immediately with the given exit code +func Fatalln(err error, exitcode int) { + fmt.Fprintln(os.Stderr, err) + os.Exit(exitcode) +} + +// Fatal prints the given err to stderr and exits immediately with the given exit code +func Fatal(err error, exitcode int) { + fmt.Fprint(os.Stderr, err) + os.Exit(exitcode) +} From 9504c8a01b06507d34439ad6a1f1c020347924cc Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Wed, 9 Sep 2020 11:55:13 +0200 Subject: [PATCH 02/11] added README --- README.md | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..94256f3 --- /dev/null +++ b/README.md @@ -0,0 +1,193 @@ +# adverr +-- + import "adverr" + +Package adverr implements errors with call stack traces as well as error +templates for error equality + + +Usage examples + +Creating templates: + + var ( + ErrDoStuffFailed = adverr.NewErrTmpl("ErrDoStuffFailed", "Could'nt do stuff because of %s") + ) + +Creating independent error (without error template): + + func doStuffWithIndependentErr() error { + return adverr.New("Could'nt do stuff") + } + +Creating error based on template: + + func doStuff() error { + return ErrDoStuffFailed.New("reasons") + } + +Printing errors on stderr convieniently: + + Print(myErr) + Println(myErr) + +Printing errors on stderr and exit with exitcode: + + Fatal(myErr, 1) + Fatalln(myErr, 1) + +Advantages of error templates + + two errors made by the same template will return true when called with errors.Is() + +## Usage + +```go +var CallStackLength int = 100 +``` +CallStackLength decides how many calls from the call stack should be gathered at +most + +```go +var ( + // ErrTmplUsedAsErr is returned from ErrTmpl.Error() because an error template should never be used as an actual error value + ErrTmplUsedAsErr = NewErrTmpl("ErrTmplUsedAsErr", "Error template used as error value: %s") +) +``` + +```go +var TraceCallStack bool = true +``` +TraceCallStack decides if any call stack traces will be gathered when creating +Errors If your application is doing performance-heavy tasks with lots of Error +creations, you may consider setting this to false If set to false, all +CallTraces in Error will be nil + +#### func Fatal + +```go +func Fatal(err error, exitcode int) +``` +Fatal prints the given err to stderr and exits immediately with the given exit +code + +#### func Fatalln + +```go +func Fatalln(err error, exitcode int) +``` +Fatalln prints the given err to stderr followed by a newline and exits +immediately with the given exit code + +#### func Print + +```go +func Print(err error) +``` +Print prints the given err to stderr + +#### func Println + +```go +func Println(err error) +``` +Println prints the given err to stderr followed by a newline + +#### type CallTrace + +```go +type CallTrace struct { +} +``` + +CallTrace represents a call stack trace similar to Java's stack trace + +#### func Trace + +```go +func Trace(skip int) *CallTrace +``` +Trace returns a new CallTrace starting from this call Use skip to skip the first +entries in the trace + +#### func (*CallTrace) String + +```go +func (ct *CallTrace) String() string +``` + +#### type ErrTmpl + +```go +type ErrTmpl struct { +} +``` + +ErrTmpl is an error template to define equalities between different errors + +#### func NewErrTmpl + +```go +func NewErrTmpl(name, format string) *ErrTmpl +``` +NewErrTmpl returns a new error template with the given format string as a +predefined error message + +#### func (*ErrTmpl) Error + +```go +func (t *ErrTmpl) Error() string +``` +Error implementation just for satisfying the error interface Please dont use +ErrTmpls as actual errors + +#### func (*ErrTmpl) New + +```go +func (t *ErrTmpl) New(args ...interface{}) *Error +``` +New returns a new Error in which the given values are being formatted into the +format string of its template + +#### type Error + +```go +type Error struct { +} +``` + +Error is a wrapper for error with stack trace + +#### func New + +```go +func New(msg string) *Error +``` +New returns a new Error with the given message + +#### func Wrap + +```go +func Wrap(msg string, cause error) *Error +``` +Wrap returns a new Error with the given message which is caused by cause + +#### func (*Error) Error + +```go +func (e *Error) Error() string +``` + +#### func (*Error) Is + +```go +func (e *Error) Is(target error) bool +``` +Is implements the error equality function used by errors.Is() It returns true if +the error is the same instance or is created using the same ErrTmpl + +#### func (*Error) Unwrap + +```go +func (e *Error) Unwrap() error +``` From 8011646a74b3fbf7314b65ebcf8c502c01e554da Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Wed, 9 Sep 2020 11:56:57 +0200 Subject: [PATCH 03/11] Revert "added README" This reverts commit 9504c8a01b06507d34439ad6a1f1c020347924cc. --- README.md | 193 ------------------------------------------------------ 1 file changed, 193 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 94256f3..0000000 --- a/README.md +++ /dev/null @@ -1,193 +0,0 @@ -# adverr --- - import "adverr" - -Package adverr implements errors with call stack traces as well as error -templates for error equality - - -Usage examples - -Creating templates: - - var ( - ErrDoStuffFailed = adverr.NewErrTmpl("ErrDoStuffFailed", "Could'nt do stuff because of %s") - ) - -Creating independent error (without error template): - - func doStuffWithIndependentErr() error { - return adverr.New("Could'nt do stuff") - } - -Creating error based on template: - - func doStuff() error { - return ErrDoStuffFailed.New("reasons") - } - -Printing errors on stderr convieniently: - - Print(myErr) - Println(myErr) - -Printing errors on stderr and exit with exitcode: - - Fatal(myErr, 1) - Fatalln(myErr, 1) - -Advantages of error templates - - two errors made by the same template will return true when called with errors.Is() - -## Usage - -```go -var CallStackLength int = 100 -``` -CallStackLength decides how many calls from the call stack should be gathered at -most - -```go -var ( - // ErrTmplUsedAsErr is returned from ErrTmpl.Error() because an error template should never be used as an actual error value - ErrTmplUsedAsErr = NewErrTmpl("ErrTmplUsedAsErr", "Error template used as error value: %s") -) -``` - -```go -var TraceCallStack bool = true -``` -TraceCallStack decides if any call stack traces will be gathered when creating -Errors If your application is doing performance-heavy tasks with lots of Error -creations, you may consider setting this to false If set to false, all -CallTraces in Error will be nil - -#### func Fatal - -```go -func Fatal(err error, exitcode int) -``` -Fatal prints the given err to stderr and exits immediately with the given exit -code - -#### func Fatalln - -```go -func Fatalln(err error, exitcode int) -``` -Fatalln prints the given err to stderr followed by a newline and exits -immediately with the given exit code - -#### func Print - -```go -func Print(err error) -``` -Print prints the given err to stderr - -#### func Println - -```go -func Println(err error) -``` -Println prints the given err to stderr followed by a newline - -#### type CallTrace - -```go -type CallTrace struct { -} -``` - -CallTrace represents a call stack trace similar to Java's stack trace - -#### func Trace - -```go -func Trace(skip int) *CallTrace -``` -Trace returns a new CallTrace starting from this call Use skip to skip the first -entries in the trace - -#### func (*CallTrace) String - -```go -func (ct *CallTrace) String() string -``` - -#### type ErrTmpl - -```go -type ErrTmpl struct { -} -``` - -ErrTmpl is an error template to define equalities between different errors - -#### func NewErrTmpl - -```go -func NewErrTmpl(name, format string) *ErrTmpl -``` -NewErrTmpl returns a new error template with the given format string as a -predefined error message - -#### func (*ErrTmpl) Error - -```go -func (t *ErrTmpl) Error() string -``` -Error implementation just for satisfying the error interface Please dont use -ErrTmpls as actual errors - -#### func (*ErrTmpl) New - -```go -func (t *ErrTmpl) New(args ...interface{}) *Error -``` -New returns a new Error in which the given values are being formatted into the -format string of its template - -#### type Error - -```go -type Error struct { -} -``` - -Error is a wrapper for error with stack trace - -#### func New - -```go -func New(msg string) *Error -``` -New returns a new Error with the given message - -#### func Wrap - -```go -func Wrap(msg string, cause error) *Error -``` -Wrap returns a new Error with the given message which is caused by cause - -#### func (*Error) Error - -```go -func (e *Error) Error() string -``` - -#### func (*Error) Is - -```go -func (e *Error) Is(target error) bool -``` -Is implements the error equality function used by errors.Is() It returns true if -the error is the same instance or is created using the same ErrTmpl - -#### func (*Error) Unwrap - -```go -func (e *Error) Unwrap() error -``` From 9d0c74df682535ecc5f70b90430337ff62ff7385 Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Wed, 9 Sep 2020 11:57:48 +0200 Subject: [PATCH 04/11] made go module --- go.mod | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 go.mod diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..83b2240 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.tordarus.net/tordarus/adverr + +go 1.15 From b33b40c9ffb6799e0564c34d22d2df164a8a1a2b Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Wed, 9 Sep 2020 13:34:23 +0200 Subject: [PATCH 05/11] added README.md once again --- README.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ error.go | 6 +--- 2 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f28c38 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# adverr + +Package adverr implements errors with call stack traces +as well as error templates for error equality + +## Installation +``` +go get git.tordarus.net/tordarus/adverr +``` + +## Usage examples + +### Importing +```go +import "git.tordarus.net/tordarus/adverr" +``` + +### Creating error templates +```go +var ( + ErrDoStuffFailed = adverr.NewErrTmpl("ErrDoStuffFailed", "Could'nt do stuff because of %s") +) +``` + +### Creating independent error (without error template) +```go +func doStuffWithIndependentErr() error { + return adverr.New("Could'nt do stuff") +} +``` + +### Creating error based on template +```go +func doStuff() error { + return ErrDoStuffFailed.New("reasons") +} +``` + +### Printing errors on stderr convieniently +```go +adverr.Print(myErr) +adverr.Println(myErr) +``` + +### Printing errors on stderr and exit with exit code +```go +adverr.Fatal(myErr, 1) +adverr.Fatalln(myErr, 1) +``` + +### Advantages of error templates +two errors made by the same template will return true when called with `errors.Is()` +```go +func doStuffAndFailWithMsg(msg string) error { + return ErrDoStuffFailed.New(msg) +} + +err1 := doStuffAndFailWithMsg("err1") +err2 := doStuffAndFailWithMsg("err2") + +fmt.Println(errors.Is(err1, err2)) // true +fmt.Println(err1 == err2) // false +fmt.Println(err1.Error() == err2.Error()) // false +``` + +### Wrapping errors +By wrapping errors, you can provide an error that is caused by another error. +A 'Caused by' section will be printed in the stack trace showing the original error. +You can also retrieve the original error by using `errors.Unwrap()` +```go +func doStuffWrapped() error { + err := doStuff() + if err != nil { + return adverr.Wrap("doStuffWrapped failed", err) + } + + return nil +} +``` + +### Retrieving call stack trace (for debugging purposes) +```go +fmt.Println(adverr.Trace()) +``` + +### \ No newline at end of file diff --git a/error.go b/error.go index bc99bae..e4f49e3 100644 --- a/error.go +++ b/error.go @@ -37,11 +37,7 @@ func errtype(err error) string { if e, ok := err.(*Error); ok && e.tmpl != nil { return errtype(e.tmpl) } else if tmpl, ok := err.(*ErrTmpl); ok { - t := reflect.TypeOf(tmpl) - for t.Kind() == reflect.Ptr { - t = t.Elem() - } - return t.PkgPath() + "." + tmpl.name + return tmpl.name } t := reflect.TypeOf(err) From 37acf646f4b06eacbafc7e4c72943d1afd1788cb Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Wed, 9 Sep 2020 14:06:31 +0200 Subject: [PATCH 06/11] README.md improved --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++- calltrace.go | 13 ++++++--- error.go | 2 +- error_tmpl.go | 10 +++++++ globals.go | 8 +++--- 5 files changed, 99 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4f28c38..99a8908 100644 --- a/README.md +++ b/README.md @@ -83,4 +83,77 @@ func doStuffWrapped() error { fmt.Println(adverr.Trace()) ``` -### \ No newline at end of file +### Example of a printed error +Code: +```go +package main + +import ( + "adverr" + "errors" +) + +var ( + ErrDoStuffFailed = adverr.NewErrTmpl("ErrDoStuffFailed", "Could'nt do stuff because of %s") +) + +func main() { + err := doStuffInAnotherGoroutine() + if err != nil { + adverr.Fatalln(err, 1) + } +} + +func doStuff() error { + err := doGoNativeStuff() + if err != nil { + return ErrDoStuffFailed.Wrap(err, "reasons") + } + + return nil +} + +func doStuffInAnotherGoroutine() error { + ch := make(chan error, 1) + + go func() { + ch <- doStuff() + close(ch) + }() + + err := <-ch + if err != nil { + return adverr.Wrap("Goroutine failed because of errors", err) + } + + return nil +} + +func doGoNativeStuff() error { + return errors.New("some go error") +} + +``` +Output: +``` +adverr.Error: Goroutine failed because of errors + at main.doStuffInAnotherGoroutine (/home/user/go/src/test/main.go:38) + at main.main (/home/user/go/src/test/main.go:13) + at runtime.main (/usr/local/go/src/runtime/proc.go:204) +Caused by ErrDoStuffFailed: Could'nt do stuff because of reasons + at main.doStuff (/home/user/go/src/test/main.go:22) + at main.doStuffInAnotherGoroutine.func1 (/home/user/go/src/test/main.go:32) +Caused by errors.errorString: some go error + (Unknown source) +``` + +### Globals +You can set the maximum limit of the call stack trace via +```go +adverr.CallStackLength = 50 // default value: 100 +``` + +If you are in a productive environment, consider disabling call traces completely for performance reasons: +```go +adverr.DisableTrace = true // default value: false +``` \ No newline at end of file diff --git a/calltrace.go b/calltrace.go index 4a99722..3368131 100644 --- a/calltrace.go +++ b/calltrace.go @@ -1,6 +1,7 @@ package adverr import ( + "fmt" "runtime" "strconv" "strings" @@ -9,19 +10,21 @@ import ( // CallTrace represents a call stack trace similar to Java's stack trace type CallTrace struct { frames *runtime.Frames + more bool } // Trace returns a new CallTrace starting from this call // Use skip to skip the first entries in the trace func Trace(skip int) *CallTrace { - if !TraceCallStack { + if DisableTrace { return nil } - pc := make([]uintptr, CallStackLength) + pc := make([]uintptr, CallStackLength+1) n := runtime.Callers(skip+1, pc) pc = pc[:n] - return &CallTrace{runtime.CallersFrames(pc)} + fmt.Println(n, CallStackLength) + return &CallTrace{runtime.CallersFrames(pc), n == CallStackLength+1} } func (ct *CallTrace) String() string { @@ -42,5 +45,9 @@ func (ct *CallTrace) String() string { b.WriteString("\n") } + if ct.more { + b.WriteString("\t ...\n") + } + return b.String() } diff --git a/error.go b/error.go index e4f49e3..749d6df 100644 --- a/error.go +++ b/error.go @@ -89,7 +89,7 @@ func printErr(err error, b *strings.Builder) { b.WriteString(errtype(err)) b.WriteString(": ") b.WriteString(err.Error()) - b.WriteString("\n") + b.WriteString("\n\t(Unknown source)\n") } cause := errors.Unwrap(err) diff --git a/error_tmpl.go b/error_tmpl.go index 11e088c..16e6e29 100644 --- a/error_tmpl.go +++ b/error_tmpl.go @@ -33,3 +33,13 @@ func (t *ErrTmpl) New(args ...interface{}) *Error { callTrace: Trace(2), } } + +// Wrap returns a new Error with a given cause in which args are being formatted into the format string of its template +func (t *ErrTmpl) Wrap(cause error, args ...interface{}) *Error { + return &Error{ + msg: fmt.Sprintf(t.format, args...), + cause: cause, + tmpl: t, + callTrace: Trace(2), + } +} diff --git a/globals.go b/globals.go index 4c6de23..dff85da 100644 --- a/globals.go +++ b/globals.go @@ -1,9 +1,9 @@ package adverr -// TraceCallStack decides if any call stack traces will be gathered when creating Errors -// If your application is doing performance-heavy tasks with lots of Error creations, you may consider setting this to false -// If set to false, all CallTraces in Error will be nil -var TraceCallStack bool = true +// DisableTrace decides if any call stack traces will be gathered when creating Errors +// If your application is doing performance-heavy tasks with lots of Error creations, you may consider setting this to true +// If set to true, all CallTraces in Error will be nil +var DisableTrace bool = true // CallStackLength decides how many calls from the call stack should be gathered at most var CallStackLength int = 100 From 023b997cb1b3ad3db9cca940d184a098d78e298d Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Wed, 9 Sep 2020 14:07:55 +0200 Subject: [PATCH 07/11] README.md further improved --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 99a8908..f8344d9 100644 --- a/README.md +++ b/README.md @@ -10,39 +10,39 @@ go get git.tordarus.net/tordarus/adverr ## Usage examples -### Importing +### Import ```go import "git.tordarus.net/tordarus/adverr" ``` -### Creating error templates +### Create error templates ```go var ( ErrDoStuffFailed = adverr.NewErrTmpl("ErrDoStuffFailed", "Could'nt do stuff because of %s") ) ``` -### Creating independent error (without error template) +### Create independent error (without error template) ```go func doStuffWithIndependentErr() error { return adverr.New("Could'nt do stuff") } ``` -### Creating error based on template +### Create error based on template ```go func doStuff() error { return ErrDoStuffFailed.New("reasons") } ``` -### Printing errors on stderr convieniently +### Print errors on stderr convieniently ```go adverr.Print(myErr) adverr.Println(myErr) ``` -### Printing errors on stderr and exit with exit code +### Print errors on stderr and exit with exit code ```go adverr.Fatal(myErr, 1) adverr.Fatalln(myErr, 1) @@ -63,7 +63,7 @@ fmt.Println(err1 == err2) // false fmt.Println(err1.Error() == err2.Error()) // false ``` -### Wrapping errors +### Wrap errors (Causality of errors) By wrapping errors, you can provide an error that is caused by another error. A 'Caused by' section will be printed in the stack trace showing the original error. You can also retrieve the original error by using `errors.Unwrap()` @@ -78,7 +78,7 @@ func doStuffWrapped() error { } ``` -### Retrieving call stack trace (for debugging purposes) +### Retrieve call stack trace (for debugging purposes) ```go fmt.Println(adverr.Trace()) ``` From eb155f7ef20fc5eb50c600e139b8ae17db1ac7dc Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Thu, 10 Sep 2020 14:39:18 +0200 Subject: [PATCH 08/11] fixed default value for DisableTrace --- globals.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/globals.go b/globals.go index dff85da..a114b46 100644 --- a/globals.go +++ b/globals.go @@ -3,7 +3,8 @@ package adverr // DisableTrace decides if any call stack traces will be gathered when creating Errors // If your application is doing performance-heavy tasks with lots of Error creations, you may consider setting this to true // If set to true, all CallTraces in Error will be nil -var DisableTrace bool = true +var DisableTrace bool = false // CallStackLength decides how many calls from the call stack should be gathered at most +// Changing this value has no effect if DisableTrace is true var CallStackLength int = 100 From 3c49668cc91c0aba83c51c6e7556ae70182c76a3 Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Thu, 10 Sep 2020 15:05:28 +0200 Subject: [PATCH 09/11] removed useless println --- calltrace.go | 2 -- error_test.go | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/calltrace.go b/calltrace.go index 3368131..1479173 100644 --- a/calltrace.go +++ b/calltrace.go @@ -1,7 +1,6 @@ package adverr import ( - "fmt" "runtime" "strconv" "strings" @@ -23,7 +22,6 @@ func Trace(skip int) *CallTrace { pc := make([]uintptr, CallStackLength+1) n := runtime.Callers(skip+1, pc) pc = pc[:n] - fmt.Println(n, CallStackLength) return &CallTrace{runtime.CallersFrames(pc), n == CallStackLength+1} } diff --git a/error_test.go b/error_test.go index 2c9b384..18edb94 100644 --- a/error_test.go +++ b/error_test.go @@ -7,7 +7,7 @@ import ( ) func TestErr(t *testing.T) { - TraceCallStack = false + DisableTrace = false err := doStuff() fmt.Println(err) } From 99f4cca565a479589fc673d2bee46185006e82a5 Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Mon, 19 Oct 2020 11:40:45 +0200 Subject: [PATCH 10/11] improved errors.Is implementation --- error.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/error.go b/error.go index 749d6df..6e977f0 100644 --- a/error.go +++ b/error.go @@ -75,6 +75,11 @@ func (e *Error) Is(target error) bool { return tErr.tmpl == e.tmpl } + // target is the template itself, therefore they are considered equal to another + if tTmpl, ok := target.(*ErrTmpl); ok { + return tTmpl == e.tmpl + } + return false } From 53d843e2e3d3092f1c23c96fb08d5d020fce0ca7 Mon Sep 17 00:00:00 2001 From: Tordarus Date: Thu, 9 Sep 2021 16:35:48 +0200 Subject: [PATCH 11/11] chained errors introduced --- README.md | 46 ++++++++++++++++++++++++++++++++++- error.go | 60 ++++++++++++++++++++++++++++++++++++++++++---- error_test.go | 13 +++++++++- error_tmpl.go | 11 +++++++++ error_tmpl_test.go | 13 ++++++++-- 5 files changed, 135 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f8344d9..ccb902e 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,31 @@ func doStuffWrapped() error { } ``` +### Chain errors (Errors caused in succession) +By chaining errors, you can provide an error that represents multiple errors caused in succession inside the same function. +For each chained error a 'Previously thrown' section will be printed in the stack trace. +You can programmatically check chained errors using the following methods: + +```go +// Get Returns the first error in the chain for which errors.Is(target) returns true +Get(target error) error + +// GetByIndex returns the i'th error in the chain +GetByIndex(i int) error + +// Chain returns a slice of all chained errors +Chain() []error + +// Contains is a shorthand for Get(target) != nil. +// Can be considered as an errors.Is function but for chains instead of causes +Contains(target error) bool +``` + +Be aware that the standard library calls wrapped errors chains as well! But these chains are something different. Here is an example use case: + +You have a list of files from which you only want to read the first one you have read permissions for. This is most likely done in a loop inside the same function. +A chained error can keep all previously failed read errors and show them in a debuggable way. Wrapping by causality would be ambiguous because they might already have been wrapped multiple times and their causes can therefore not be distinguished from previously failed errors (chained errors). + ### Retrieve call stack trace (for debugging purposes) ```go fmt.Println(adverr.Trace()) @@ -156,4 +181,23 @@ adverr.CallStackLength = 50 // default value: 100 If you are in a productive environment, consider disabling call traces completely for performance reasons: ```go adverr.DisableTrace = true // default value: false -``` \ No newline at end of file +``` + +## Change log + +### v0.1.2 + +Introduced error chaining + +### v0.1.1 + +Improved errors.Is behavior so that ErrTmpl's are considered as targets as well. Example: + +```go +err := ErrDoStuffFailed.New("some error") +fmt.Println(errors.Is(err, ErrDoStuffFailed)) // returns true since v0.1.1 +``` + +### v0.1.0 + +initial release \ No newline at end of file diff --git a/error.go b/error.go index 6e977f0..b7370ec 100644 --- a/error.go +++ b/error.go @@ -12,14 +12,13 @@ type Error struct { callTrace *CallTrace tmpl *ErrTmpl cause error + prev []error } // New returns a new Error with the given message func New(msg string) *Error { return &Error{ msg: msg, - cause: nil, - tmpl: nil, callTrace: Trace(2), } } @@ -33,6 +32,16 @@ func Wrap(msg string, cause error) *Error { } } +// Chain returns a new Error with the given message and a slice of errors +// which were caused in the same function in succession +func Chain(msg string, errors []error) *Error { + return &Error{ + msg: msg, + callTrace: Trace(2), + prev: errors, + } +} + func errtype(err error) string { if e, ok := err.(*Error); ok && e.tmpl != nil { return errtype(e.tmpl) @@ -83,8 +92,43 @@ func (e *Error) Is(target error) bool { return false } +// Get Returns the first error in the chain for which errors.Is(target) returns true +func (e *Error) Get(target error) error { + if e.prev == nil { + return nil + } + + for _, prevErr := range e.prev { + if errors.Is(prevErr, target) { + return prevErr + } + } + return nil +} + +// GetByIndex returns the i'th error in the chain +func (e *Error) GetByIndex(i int) error { + if e.prev == nil { + return nil + } + return e.prev[i] +} + +// Contains is a shorthand for Get(target) != nil. +// Can be considered as an errors.Is function but for chains instead of causes +func (e *Error) Contains(target error) bool { + return e.Get(target) != nil +} + +// Chain returns a slice of all chained errors +func (e *Error) Chain() []error { + return e.prev[:] +} + func printErr(err error, b *strings.Builder) { - if e, ok := err.(*Error); ok { + e, ok := err.(*Error) + + if ok { b.WriteString(errtype(e)) b.WriteString(": ") b.WriteString(e.msg) @@ -94,7 +138,8 @@ func printErr(err error, b *strings.Builder) { b.WriteString(errtype(err)) b.WriteString(": ") b.WriteString(err.Error()) - b.WriteString("\n\t(Unknown source)\n") + b.WriteString("\n") + b.WriteString("\t(Unknown source)\n") } cause := errors.Unwrap(err) @@ -102,4 +147,11 @@ func printErr(err error, b *strings.Builder) { b.WriteString("Caused by ") printErr(cause, b) } + + if ok { + for _, prevErr := range e.prev { + b.WriteString("Previously thrown ") + printErr(prevErr, b) + } + } } diff --git a/error_test.go b/error_test.go index 18edb94..db37523 100644 --- a/error_test.go +++ b/error_test.go @@ -9,9 +9,20 @@ import ( func TestErr(t *testing.T) { DisableTrace = false err := doStuff() - fmt.Println(err) + Println(err) } func doStuff() error { return Wrap("wrapped error", Wrap("test error", fmt.Errorf("asd: %w", errors.New("test")))) } + +func TestErrorChain(t *testing.T) { + + errors := make([]error, 0) + + errors = append(errors, doStuff()) + errors = append(errors, doStuff()) + errors = append(errors, doStuff()) + + Println(Chain("Neither of that stuff worked", errors)) +} diff --git a/error_tmpl.go b/error_tmpl.go index 16e6e29..1d51860 100644 --- a/error_tmpl.go +++ b/error_tmpl.go @@ -43,3 +43,14 @@ func (t *ErrTmpl) Wrap(cause error, args ...interface{}) *Error { callTrace: Trace(2), } } + +// Chain returns a new Error with the given message and a slice of errors +// which were caused in the same function in succession +func (t *ErrTmpl) Chain(msg string, errors []error) *Error { + return &Error{ + msg: msg, + callTrace: Trace(2), + tmpl: t, + prev: errors, + } +} diff --git a/error_tmpl_test.go b/error_tmpl_test.go index 32629f5..401531b 100644 --- a/error_tmpl_test.go +++ b/error_tmpl_test.go @@ -1,7 +1,6 @@ package adverr import ( - "fmt" "testing" ) @@ -11,9 +10,19 @@ var ( func TestErrTmpl(t *testing.T) { err := doTemplateStuff() - fmt.Println(err) + Println(err) } func doTemplateStuff() error { return ErrDoStuffFailed.New("because of reasons") } + +func TestErrTmplChain(t *testing.T) { + errors := make([]error, 0) + + errors = append(errors, doTemplateStuff()) + errors = append(errors, doTemplateStuff()) + errors = append(errors, doTemplateStuff()) + + Println(ErrDoStuffFailed.Chain("Neither of that stuff worked", errors)) +}