From f19e3e383e035039147ae136044d46916fe3d91f Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 09:49:40 +0000 Subject: [PATCH] fix(log): inherit nested error codes --- errors.go | 13 ++++++++++++- errors_test.go | 11 +++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/errors.go b/errors.go index 28bfc42..5064555 100644 --- a/errors.go +++ b/errors.go @@ -104,7 +104,7 @@ func Wrap(err error, op, msg string) error { if err == nil { return nil } - wrapped := &Err{Op: op, Msg: msg, Err: err, Code: ErrCode(err)} + wrapped := &Err{Op: op, Msg: msg, Err: err, Code: inheritedCode(err)} inheritRecovery(wrapped, err) return wrapped } @@ -201,6 +201,17 @@ func inheritRecovery(dst *Err, err error) { } } +// inheritedCode returns the first non-empty code found in an error chain. +func inheritedCode(err error) string { + for err != nil { + if wrapped, ok := err.(*Err); ok && wrapped.Code != "" { + return wrapped.Code + } + err = errors.Unwrap(err) + } + return "" +} + // RetryAfter returns the first retry-after hint from an error chain, if present. // // retryAfter, ok := log.RetryAfter(err) diff --git a/errors_test.go b/errors_test.go index 365ca1f..9dde46e 100644 --- a/errors_test.go +++ b/errors_test.go @@ -124,6 +124,17 @@ func TestWrap_PreservesCode_Good(t *testing.T) { assert.Contains(t, outer.Error(), "[VALIDATION_ERROR]") } +func TestWrap_PreservesCode_FromNestedErrWithEmptyOuterCode_Good(t *testing.T) { + inner := WrapCode(errors.New("base"), "VALIDATION_ERROR", "inner.Op", "validation failed") + mid := &Err{Op: "mid.Op", Msg: "mid failed", Err: inner} + + outer := Wrap(mid, "outer.Op", "outer context") + + assert.NotNil(t, outer) + assert.Equal(t, "VALIDATION_ERROR", ErrCode(outer)) + assert.Contains(t, outer.Error(), "[VALIDATION_ERROR]") +} + func TestWrap_PreservesRecovery_Good(t *testing.T) { retryAfter := 15 * time.Second inner := &Err{Msg: "inner", Retryable: true, RetryAfter: &retryAfter, NextAction: "inspect input"} -- 2.45.3