Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
# failure
# 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())
}
```
80 changes: 80 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package failure

import (
"fmt"
"errors"
)

type Errno int32
Expand All @@ -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 {
Expand All @@ -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}
}
Expand All @@ -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
}
25 changes: 24 additions & 1 deletion error_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
36 changes: 20 additions & 16 deletions traceback.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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) }
Expand Down
7 changes: 7 additions & 0 deletions traceback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}