diff --git a/errors.go b/errors.go index 161aea25..68ba6d98 100644 --- a/errors.go +++ b/errors.go @@ -109,13 +109,93 @@ func New(message string) error { // Errorf formats according to a format specifier and returns the string // as a value that satisfies error. // Errorf also records the stack trace at the point it was called. +// If the format specifier includes a %w verb with an error operand, +// the returned error will implement an Unwrap method returning the operand. func Errorf(format string, args ...interface{}) error { + err := fmt.Errorf(format, args...) + if u, ok := err.(interface{ Unwrap() error }); ok { + return &wrapError{ + msg: err.Error(), + err: u.Unwrap(), + stack: callers(), + } + } + if u, ok := err.(interface{ Unwrap() []error }); ok { + return &wrapErrors{ + msg: err.Error(), + errs: u.Unwrap(), + stack: callers(), + } + } return &fundamental{ - msg: fmt.Sprintf(format, args...), + msg: err.Error(), stack: callers(), } } +type wrapError struct { + msg string + err error + *stack +} + +func (w *wrapError) Error() string { return w.msg } + +func (w *wrapError) Cause() error { return w.err } + +// Unwrap provides compatibility for Go 1.13 error chains. +func (w *wrapError) Unwrap() error { return w.err } + +func (w *wrapError) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, w.msg) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.msg) + case 'q': + fmt.Fprintf(s, "%q", w.msg) + } +} + +type wrapErrors struct { + msg string + errs []error + *stack +} + +func (w *wrapErrors) Error() string { return w.msg } + +func (w *wrapErrors) Cause() error { + if len(w.errs) > 0 { + return w.errs[0] + } + return nil +} + +// Unwrap provides compatibility for Go 1.20 error chains. +func (w *wrapErrors) Unwrap() []error { return w.errs } + +func (w *wrapErrors) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, w.msg) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.msg) + case 'q': + fmt.Fprintf(s, "%q", w.msg) + } +} + // fundamental is an error that has a message and a stack, but no caller. type fundamental struct { msg string diff --git a/errors_test.go b/errors_test.go index 2089b2f7..bf411a3d 100644 --- a/errors_test.go +++ b/errors_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "reflect" + "strings" "testing" ) @@ -139,6 +140,10 @@ func TestErrorf(t *testing.T) { }{ {Errorf("read error without format specifiers"), "read error without format specifiers"}, {Errorf("read error with %d format specifier", 1), "read error with 1 format specifier"}, + {Errorf("read error: %w", io.EOF), "read error: EOF"}, + {Errorf("%w: client error", io.EOF), "EOF: client error"}, + {Errorf("read %d bytes error: %w", 42, io.EOF), "read 42 bytes error: EOF"}, + {Errorf("%w", io.EOF), "EOF"}, } for _, tt := range tests { @@ -149,6 +154,107 @@ func TestErrorf(t *testing.T) { } } +func TestErrorfWrapping(t *testing.T) { + base := errors.New("base error") + wrapped := Errorf("wrapped: %w", base) + + // Verify Unwrap() error + u, ok := wrapped.(interface{ Unwrap() error }) + if !ok { + t.Fatal("expected wrapped error to implement Unwrap() error") + } + if got := u.Unwrap(); got != base { + t.Errorf("Unwrap() = %v, want %v", got, base) + } + + // Verify Cause() error + c, ok := wrapped.(interface{ Cause() error }) + if !ok { + t.Fatal("expected wrapped error to implement Cause() error") + } + if got := c.Cause(); got != base { + t.Errorf("Cause() = %v, want %v", got, base) + } + if got := Cause(wrapped); got != base { + t.Errorf("Cause(wrapped) = %v, want %v", got, base) + } + + // Verify nested Cause and Unwrap chains + outer := Errorf("outer: %w", wrapped) + uOuter, ok := outer.(interface{ Unwrap() error }) + if !ok { + t.Fatal("expected outer error to implement Unwrap() error") + } + if got := uOuter.Unwrap(); got != wrapped { + t.Errorf("outer.Unwrap() = %v, want %v", got, wrapped) + } + if got := Cause(outer); got != base { + t.Errorf("Cause(outer) = %v, want %v", got, base) + } + + // Verify non-%w does not implement Unwrap() error + plain := Errorf("plain %s", "error") + if _, ok := plain.(interface{ Unwrap() error }); ok { + t.Errorf("Errorf without %%w should not implement Unwrap() error") + } + + // Verify %w with nil error operand + var nilErr error + wrapNil := Errorf("wrap nil: %w", nilErr) + uNil, ok := wrapNil.(interface{ Unwrap() error }) + if !ok { + t.Fatal("expected wrapNil to implement Unwrap() error") + } + if got := uNil.Unwrap(); got != nil { + t.Errorf("wrapNil.Unwrap() = %v, want nil", got) + } + + // Verify StackTrace + st, ok := wrapped.(interface{ StackTrace() StackTrace }) + if !ok { + t.Fatal("expected wrapped error to implement StackTrace()") + } + if len(st.StackTrace()) == 0 { + t.Error("expected non-empty stack trace") + } + + // Verify formatting verbs + if got := fmt.Sprintf("%s", wrapped); got != "wrapped: base error" { + t.Errorf("%%s format = %q, want %q", got, "wrapped: base error") + } + if got := fmt.Sprintf("%v", wrapped); got != "wrapped: base error" { + t.Errorf("%%v format = %q, want %q", got, "wrapped: base error") + } + if got := fmt.Sprintf("%q", wrapped); got != `"wrapped: base error"` { + t.Errorf("%%q format = %q, want %q", got, `"wrapped: base error"`) + } + plusV := fmt.Sprintf("%+v", wrapped) + if !strings.Contains(plusV, "wrapped: base error") || !strings.Contains(plusV, "TestErrorfWrapping") { + t.Errorf("%%+v format missing expected message or stack: %q", plusV) + } +} + +func TestErrorfMultiWrap(t *testing.T) { + err1 := errors.New("err1") + err2 := errors.New("err2") + multi := Errorf("first: %w, second: %w", err1, err2) + + u, ok := multi.(interface{ Unwrap() []error }) + if !ok { + t.Fatal("expected multi-wrapped error to implement Unwrap() []error") + } + errs := u.Unwrap() + if len(errs) != 2 || errs[0] != err1 || errs[1] != err2 { + t.Errorf("multi.Unwrap() = %v, want [%v, %v]", errs, err1, err2) + } + if got := Cause(multi); got != err1 { + t.Errorf("Cause(multi) = %v, want %v", got, err1) + } + if got := multi.Error(); got != "first: err1, second: err2" { + t.Errorf("multi.Error() = %q, want %q", got, "first: err1, second: err2") + } +} + func TestWithStackNil(t *testing.T) { got := WithStack(nil) if got != nil { diff --git a/go113_test.go b/go113_test.go index 4ea37e61..4b31f85d 100644 --- a/go113_test.go +++ b/go113_test.go @@ -61,6 +61,14 @@ func TestIs(t *testing.T) { }, want: true, }, + { + name: "errorf with %w", + args: args{ + err: Errorf("wrap it: %w", err), + target: err, + }, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -121,6 +129,14 @@ func TestAs(t *testing.T) { }, want: true, }, + { + name: "errorf with %w", + args: args{ + err: Errorf("wrap it: %w", err), + target: new(customErr), + }, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -167,6 +183,11 @@ func TestUnwrap(t *testing.T) { args: args{err: fmt.Errorf("wrap: %w", err)}, want: err, }, + { + name: "errorf with %w", + args: args{err: Errorf("wrap: %w", err)}, + want: err, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {