diff --git a/README.md b/README.md index 9f21e13..144b059 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,19 @@ errors.Wrap(err, "message") * The [cockroachdb/errors](https://github.com/cockroachdb/errors) is too big * The `errors` has no caller stack trace +## Wrap on nil + +`Wrap(nil, "msg")` returns a **non-nil** error, matching `fmt.Errorf("msg: %w", err)` +with a nil `err` (and unlike `pkg/errors`, which returns nil). Wrap only after +checking `err != nil`: + +```go +if err := do(); err != nil { + return errors.Wrap(err, "do") +} +return nil +``` + ## Don't need traces? Call `errors.DisableTrace` or use build tag `noerrtrace`. diff --git a/format.go b/format.go index b83d9cb..798695a 100644 --- a/format.go +++ b/format.go @@ -39,9 +39,19 @@ type Printer interface { } // Errorf creates new error with format. +// +// If format contains the %w directive, Errorf falls back to fmt.Errorf +// and does not capture a caller frame. func Errorf(format string, a ...interface{}) error { if !Trace() || strings.Contains(format, "%w") { return fmt.Errorf(format, a...) } - return &errorString{fmt.Sprintf(format, a...), Caller(1)} + return &errorString{sprintf(format, a), Caller(1)} +} + +// sprintf is deliberately non-variadic: forwarding (format, a...) straight +// to fmt.Sprintf from Errorf makes vet's printf analyzer classify Errorf as +// a plain printf wrapper, rejecting %w in callers' format strings. +func sprintf(format string, a []interface{}) string { + return fmt.Sprintf(format, a...) } diff --git a/wrap.go b/wrap.go index 61f008f..1aa7426 100644 --- a/wrap.go +++ b/wrap.go @@ -78,6 +78,10 @@ func (e *wrapError) Unwrap() error { } // Wrap error with message and caller. +// +// Wrap always returns a non-nil error, even if err is nil, matching the +// behavior of fmt.Errorf with a nil %w operand. Call Wrap only after +// checking that err is non-nil. func Wrap(err error, message string) error { frame := Frame{} if Trace() { @@ -87,6 +91,8 @@ func Wrap(err error, message string) error { } // Wrapf wraps error with formatted message and caller. +// +// Like Wrap, Wrapf always returns a non-nil error, even if err is nil. func Wrapf(err error, format string, a ...interface{}) error { frame := Frame{} if Trace() { diff --git a/wrap_test.go b/wrap_test.go index 155321c..5339654 100644 --- a/wrap_test.go +++ b/wrap_test.go @@ -209,6 +209,19 @@ func TestUnwrap(t *testing.T) { } } +func TestWrapNil(t *testing.T) { + // Wrap and Wrapf return a non-nil error even for a nil input, matching + // fmt.Errorf with a nil %w operand. See the Wrap doc comment. + if err := errors.Wrap(nil, "foo"); err == nil { + t.Error("Wrap(nil) = nil, want non-nil") + } else if got := errors.Unwrap(err); got != nil { + t.Errorf("Unwrap(Wrap(nil)) = %v, want nil", got) + } + if err := errors.Wrapf(nil, "foo %d", 1); err == nil { + t.Error("Wrapf(nil) = nil, want non-nil") + } +} + func TestOpaque(t *testing.T) { got := fmt.Sprintf("%v", errors.Wrap(errors.Opaque(errorT{}), "foo")) want := "foo: errorT"