Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 106 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"reflect"
"strings"
"testing"
)

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions go113_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down