From 53d843e2e3d3092f1c23c96fb08d5d020fce0ca7 Mon Sep 17 00:00:00 2001 From: Tordarus Date: Thu, 9 Sep 2021 16:35:48 +0200 Subject: [PATCH] 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)) +}