From c146378ed4c45678505d288e2a7ba3415885e563 Mon Sep 17 00:00:00 2001 From: Jay Petacat Date: Tue, 12 Nov 2019 12:42:33 -0500 Subject: [PATCH] Support Go 1.13 error chains in `Cause` Imagine module A imports module B and both use `pkg/errors`. A uses `errors.Cause` to inspect wrapped errors returned from B. As-is, B cannot migrate from `errors.Wrap` to `fmt.Errorf("%w", err)` because that would break `errors.Cause` calls in A. With this change merged, `errors.Cause` becomes forwards-compatible with Go 1.13 error chains. Module B will be free to switch to `fmt.Errorf("%w", err)` and that will not break module A (so long as the top-level project pulls in the newer version of `pkg/errors`). --- cause.go | 29 +++++++++++++++++++++++++++++ cause_go113.go | 37 +++++++++++++++++++++++++++++++++++++ errors.go | 26 -------------------------- go113_test.go | 12 +++++++++++- 4 files changed, 77 insertions(+), 27 deletions(-) create mode 100644 cause.go create mode 100644 cause_go113.go diff --git a/cause.go b/cause.go new file mode 100644 index 0000000..2e9d273 --- /dev/null +++ b/cause.go @@ -0,0 +1,29 @@ +// +build !go1.13 + +package errors + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/cause_go113.go b/cause_go113.go new file mode 100644 index 0000000..2ec4398 --- /dev/null +++ b/cause_go113.go @@ -0,0 +1,37 @@ +// +build go1.13 + +package errors + +import "errors" + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement the Cause interface, the Cause +// function will fallback to the standard library's errors.Unwrap +// that was added in Go 1.13. This makes Cause forwards-compatible +// with Go 1.13 error chains. +// +// If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + if cause, ok := err.(causer); ok { + err = cause.Cause() + } else if unwrapped := errors.Unwrap(err); unwrapped != nil { + err = unwrapped + } else { + break + } + } + return err +} diff --git a/errors.go b/errors.go index 161aea2..a9840ec 100644 --- a/errors.go +++ b/errors.go @@ -260,29 +260,3 @@ func (w *withMessage) Format(s fmt.State, verb rune) { io.WriteString(s, w.Error()) } } - -// Cause returns the underlying cause of the error, if possible. -// An error value has a cause if it implements the following -// interface: -// -// type causer interface { -// Cause() error -// } -// -// If the error does not implement Cause, the original error will -// be returned. If the error is nil, nil will be returned without further -// investigation. -func Cause(err error) error { - type causer interface { - Cause() error - } - - for err != nil { - cause, ok := err.(causer) - if !ok { - break - } - err = cause.Cause() - } - return err -} diff --git a/go113_test.go b/go113_test.go index 39263b0..c52c7c7 100644 --- a/go113_test.go +++ b/go113_test.go @@ -4,10 +4,20 @@ package errors import ( stdlib_errors "errors" + "fmt" "testing" ) -func TestErrorChainCompat(t *testing.T) { +func TestCauseErrorChainCompat(t *testing.T) { + err := stdlib_errors.New("leaf error") + wrapped := fmt.Errorf("wrapped error: %w", err) + wrapped = fmt.Errorf("wrapped again: %w", err) + if Cause(wrapped) != err { + t.Errorf("Cause does not support Go 1.13 error chains") + } +} + +func TestWrapErrorChainCompat(t *testing.T) { err := stdlib_errors.New("error that gets wrapped") wrapped := Wrap(err, "wrapped up") if !stdlib_errors.Is(wrapped, err) {