From 308faaf99628b253e5f0d2a80393d9c1693edcce Mon Sep 17 00:00:00 2001 From: Aleksandr Razumov Date: Fri, 24 Jul 2026 19:10:48 +0300 Subject: [PATCH 1/2] docs: state that Wrap(nil) returns non-nil, pin with test Wrap and Wrapf always return a non-nil error even for a nil input, matching fmt.Errorf with a nil %w operand (and unlike pkg/errors). This is deliberate: gowrapper's Errorf-to-Wrap rewrite is only semantics-preserving because both sides agree on nil. Document the contract in godoc and README, and add TestWrapNil so it cannot change silently. --- README.md | 13 +++++++++++++ wrap.go | 6 ++++++ wrap_test.go | 13 +++++++++++++ 3 files changed, 32 insertions(+) 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/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" From 0e7f0b25731f1d24ec0f662bd1db78f379554b22 Mon Sep 17 00:00:00 2001 From: Aleksandr Razumov Date: Fri, 24 Jul 2026 19:10:49 +0300 Subject: [PATCH 2/2] fix: make vet recognize %w support in Errorf Errorf falls back to fmt.Errorf when the format contains %w, but vet's printf analyzer flagged callers with 'does not support error-wrapping directive %w'. The analyzer classifies a wrapper by the calls that forward (format, a...): the fmt.Sprintf forward on the traced path downgraded Errorf from errorf-like to printf-like. Move the Sprintf call behind a non-variadic helper so the only printf forward left is fmt.Errorf. No behavior change; 'go test ./...' now passes with vet enabled. --- format.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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...) }