From c4b979b43eacdada53c78d861740c490f5af81fb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 23 Sep 2025 03:57:31 +0000 Subject: [PATCH] feat: Add error wrapping, formatting, and traceback capture Co-authored-by: lixiaohui0812 --- README.md | 32 ++++++++++++++++++- error.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++ error_test.go | 25 ++++++++++++++- traceback.go | 36 +++++++++++---------- traceback_test.go | 7 +++++ 5 files changed, 162 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6d7dc84..ee8eb5d 100644 --- a/README.md +++ b/README.md @@ -1 +1,31 @@ -# failure \ No newline at end of file +# failure + +Lightweight error package with codes, wrapping, and traceback capture. + +## Quick Start + +```go +e := failure.New(1000, "bad request") +fmt.Printf("%+v\n", e) + +base := fmt.Errorf("io failed") +e2 := failure.Wrapf(base, 2001, "reading %s", "config") + +// Formats +fmt.Printf("%v\n", e2) // default +fmt.Printf("%+v\n", e2) // verbose with location +fmt.Printf("%s\n", e2) // message only + +// Helpers +code := failure.CodeOf(e2) // 2001 +e3 := e2.WithMessage("retriable").WithCode(3002) +``` + +## Traceback + +```go +frames := failure.Traceback() +for _, f := range frames { + fmt.Println(f.GetLocation(), f.GetFuncName()) +} +``` \ No newline at end of file diff --git a/error.go b/error.go index 046e4d1..6ad2cb6 100644 --- a/error.go +++ b/error.go @@ -2,6 +2,7 @@ package failure import ( "fmt" + "errors" ) type Errno int32 @@ -22,6 +23,28 @@ func New(code Errno, msg string) *Error { return &Error{Code: code, Msg: msg, frame: frame} } +// Newf creates a new Error with formatted message and captured frame. +func Newf(code Errno, format string, args ...interface{}) *Error { + return New(code, fmt.Sprintf(format, args...)) +} + +// Wrap wraps an existing error with a code and message, capturing the caller frame. +func Wrap(err error, code Errno, msg string) *Error { + if err == nil { + return nil + } + e := New(code, msg) + return e.WithError(err) +} + +// Wrapf wraps an existing error with a code and formatted message, capturing the caller frame. +func Wrapf(err error, code Errno, format string, args ...interface{}) *Error { + if err == nil { + return nil + } + return Wrap(err, code, fmt.Sprintf(format, args...)) +} + func (e *Error) Error() string { if e.inner != nil { if e.frame != nil { @@ -38,6 +61,41 @@ func (e *Error) Error() string { func (e *Error) Unwrap() error { return e.inner } +// Format implements fmt.Formatter for richer formatting behaviors. +// +// %v -> same as Error() +// %+v -> includes location if available and recursively formats inner with %+v +// %s -> message only +// %q -> quoted message +func (e *Error) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + if e.frame != nil { + if e.inner != nil { + fmt.Fprintf(s, "location: [%s] code: %d, msg: %s, err: %+v", e.frame.Location, e.Code, e.Msg, e.inner) + } else { + fmt.Fprintf(s, "location: [%s] code: %d, msg: %s", e.frame.Location, e.Code, e.Msg) + } + } else { + if e.inner != nil { + fmt.Fprintf(s, "code: %d, msg: %s, err: %+v", e.Code, e.Msg, e.inner) + } else { + fmt.Fprintf(s, "code: %d, msg: %s", e.Code, e.Msg) + } + } + return + } + fmt.Fprint(s, e.Error()) + case 's': + fmt.Fprint(s, e.Msg) + case 'q': + fmt.Fprintf(s, "%q", e.Msg) + default: + fmt.Fprint(s, e.Error()) + } +} + func (e *Error) WithError(err error) *Error { return &Error{inner: err, frame: e.Frame(), Msg: e.Msg, Code: e.Code} } @@ -46,9 +104,31 @@ func (e *Error) WithFrame(f *Frame) *Error { return &Error{inner: e.inner, frame: f, Msg: e.Msg, Code: e.Code} } +// WithMessage returns a new Error with the same code and frame but new message. +func (e *Error) WithMessage(msg string) *Error { + return &Error{inner: e.inner, frame: e.Frame(), Msg: msg, Code: e.Code} +} + +// WithCode returns a new Error with the same message and frame but new code. +func (e *Error) WithCode(code Errno) *Error { + return &Error{inner: e.inner, frame: e.Frame(), Msg: e.Msg, Code: code} +} + func (e *Error) Frame() *Frame { if e.frame == nil { return nil } return &Frame{Location: e.frame.Location, FuncName: e.frame.FuncName} } + +// CodeOf walks an error chain to find the first failure.Error and returns its code. +// If no failure.Error is found, returns 0. +func CodeOf(err error) Errno { + for err != nil { + if fe, ok := err.(*Error); ok { + return fe.Code + } + err = errors.Unwrap(err) + } + return 0 +} diff --git a/error_test.go b/error_test.go index ced5674..92a22de 100644 --- a/error_test.go +++ b/error_test.go @@ -1,9 +1,32 @@ package failure -import "testing" +import ( + "fmt" + "testing" +) func TestError(t *testing.T) { e := New(Errno(0), "success") t.Logf("%+v\n", e) t.Logf("%+v\n", e.frame) } + +func TestWrapAndFormat(t *testing.T) { + base := fmt.Errorf("io failed") + e := Wrapf(base, 1001, "reading %s", "config") + if e == nil { + t.Fatal("expected non-nil") + } + if CodeOf(e) != 1001 { + t.Fatalf("unexpected code: %d", CodeOf(e)) + } + t.Logf("%v", e) + t.Logf("%+v", e) + t.Logf("%s", e) + t.Logf("%q", e) + + e2 := e.WithMessage("new message").WithCode(2002) + if CodeOf(e2) != 2002 { + t.Fatalf("unexpected code after change: %d", CodeOf(e2)) + } +} diff --git a/traceback.go b/traceback.go index e4b884b..3fef12d 100644 --- a/traceback.go +++ b/traceback.go @@ -12,7 +12,8 @@ type Frame struct { FuncName string } -func (f *Frame) GetLoction() string { +// GetLocation returns the file:line location or ??? if nil. +func (f *Frame) GetLocation() string { if f != nil { return f.Location } @@ -29,21 +30,24 @@ func (f *Frame) GetFuncName() string { type Frames []*Frame func traceback(skip int, depth int) Frames { - var frames Frames - if depth <= 0 { - depth = maxCallerDepth - } - pc := make([]uintptr, depth) - n := runtime.Callers(skip+2, pc) - for i := 0; i < n; i++ { - fn := runtime.FuncForPC(pc[i]) - f, l := fn.FileLine(pc[i]) - frames = append(frames, &Frame{ - Location: fmt.Sprintf("%s:%d", f, l), - FuncName: fn.Name(), - }) - } - return frames + var frames Frames + if depth <= 0 { + depth = maxCallerDepth + } + pcs := make([]uintptr, depth) + n := runtime.Callers(skip+2, pcs) + cf := runtime.CallersFrames(pcs[:n]) + for { + fr, more := cf.Next() + frames = append(frames, &Frame{ + Location: fmt.Sprintf("%s:%d", fr.File, fr.Line), + FuncName: fr.Function, + }) + if !more { + break + } + } + return frames } func Traceback() Frames { return traceback(1, 0) } diff --git a/traceback_test.go b/traceback_test.go index e5ad58d..c850ebc 100644 --- a/traceback_test.go +++ b/traceback_test.go @@ -16,3 +16,10 @@ func a() Frames { func b() Frames { return a() } + +func TestGetLocationName(t *testing.T) { + f := CurrentTraceback() + if f.GetLocation() == "???" || f.GetFuncName() == "???" { + t.Fatalf("unexpected fallback values: %+v", f) + } +}