From 080f1cde8edb456ae2e4f64e1b6b805f83af52fb Mon Sep 17 00:00:00 2001 From: Adam Magued Date: Fri, 2 Oct 2026 21:43:45 +0000 Subject: [PATCH] Add WrapNil and WrapfNil nil-safe error wrapping helpers --- README.md | 16 ++++++++++++++++ errwrap.go | 25 +++++++++++++++++++++++++ errwrap_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/README.md b/README.md index 444df08..ae4006c 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,22 @@ func main() { } ``` +#### Nil-Safe Wrapping + +`WrapNil` and `WrapfNil` return `nil` if the wrapped error is `nil`, avoiding the need for an explicit `if err != nil` check before wrapping: + +```go +func tryOpen() error { + _, err := os.Open("/i/dont/exist") + return errwrap.WrapfNil("Doesn't exist: {{err}}", err) +} + +func doStep() error { + err := step() + return errwrap.WrapNil(errors.New("step failed"), err) +} +``` + #### Custom Types If you're already making custom types that properly wrap errors, then diff --git a/errwrap.go b/errwrap.go index 2b382d2..f71a8be 100644 --- a/errwrap.go +++ b/errwrap.go @@ -41,6 +41,16 @@ func Wrap(outer, inner error) error { } } +// WrapNil wraps outer around inner if inner is not nil. If inner is nil, +// it returns nil. +func WrapNil(outer, inner error) error { + if inner == nil { + return nil + } + + return Wrap(outer, inner) +} + // Wrapf wraps an error with a formatting message. This is similar to using // `fmt.Errorf` to wrap an error. If you're using `fmt.Errorf` to wrap // errors, you should replace it with this. @@ -61,6 +71,21 @@ func Wrapf(format string, err error) error { return Wrap(outer, err) } +// WrapfNil wraps an error with a formatting message if err is not nil. +// If err is nil, it returns nil. +// +// format is the format of the error message. The string '{{err}}' will +// be replaced with the original error message. +// +// Deprecated: Use fmt.Errorf() +func WrapfNil(format string, err error) error { + if err == nil { + return nil + } + + return Wrapf(format, err) +} + // Contains checks if the given error contains an error with the // message msg. If err is not a wrapped error, this will always return // false unless the error itself happens to match this msg. diff --git a/errwrap_test.go b/errwrap_test.go index 1efd865..de04cfd 100644 --- a/errwrap_test.go +++ b/errwrap_test.go @@ -120,3 +120,53 @@ func TestWrappedError_IsCompatibleWithErrorsUnwrap(t *testing.T) { t.Fatal("wrappedError did not unwrap to inner") } } + +func TestWrapNil(t *testing.T) { + if err := WrapNil(errors.New("outer"), nil); err != nil { + t.Fatalf("expected nil when inner is nil, got: %v", err) + } + + if err := WrapNil(nil, nil); err != nil { + t.Fatalf("expected nil when both are nil, got: %v", err) + } + + inner := errors.New("inner") + outer := errors.New("outer") + err := WrapNil(outer, inner) + if err == nil { + t.Fatal("expected non-nil error when inner is non-nil") + } + if err.Error() != "outer" { + t.Fatalf("expected error %q, got %q", "outer", err.Error()) + } + if !Contains(err, "inner") { + t.Fatal("expected error to contain inner") + } + if !Contains(err, "outer") { + t.Fatal("expected error to contain outer") + } + if actual := errors.Unwrap(err); actual != inner { + t.Fatalf("expected unwrapped error to be inner, got %v", actual) + } +} + +func TestWrapfNil(t *testing.T) { + if err := WrapfNil("failed: {{err}}", nil); err != nil { + t.Fatalf("expected nil when err is nil, got: %v", err) + } + + inner := errors.New("inner") + err := WrapfNil("failed: {{err}}", inner) + if err == nil { + t.Fatal("expected non-nil error when err is non-nil") + } + if err.Error() != "failed: inner" { + t.Fatalf("expected error %q, got %q", "failed: inner", err.Error()) + } + if !Contains(err, "inner") { + t.Fatal("expected error to contain inner") + } + if actual := errors.Unwrap(err); actual != inner { + t.Fatalf("expected unwrapped error to be inner, got %v", actual) + } +}