| 1 | // Package culpa provides structured error handling with typed details, |
| 2 | // stacktrace capture, and chain traversal. |
| 3 | package culpa |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "strings" |
| 8 | ) |
| 9 | |
| 10 | // culpaError is the message-wrapping error type. It holds a message |
| 11 | // and an optional cause (inner error). |
| 12 | type culpaError struct { |
| 13 | msg string |
| 14 | cause error |
| 15 | } |
| 16 | |
| 17 | // Error returns the message chain: "outer: middle: inner". |
| 18 | func (e *culpaError) Error() string { |
| 19 | if e.cause == nil { |
| 20 | return e.msg |
| 21 | } |
| 22 | causeMsg := e.cause.Error() |
| 23 | if e.msg == "" { |
| 24 | return causeMsg |
| 25 | } |
| 26 | if causeMsg == "" { |
| 27 | return e.msg |
| 28 | } |
| 29 | return e.msg + ": " + causeMsg |
| 30 | } |
| 31 | |
| 32 | // Unwrap returns the wrapped cause error. |
| 33 | func (e *culpaError) Unwrap() error { |
| 34 | return e.cause |
| 35 | } |
| 36 | |
| 37 | // joinedError wraps multiple errors so that errors.Is/As can find any of them. |
| 38 | type joinedError struct { |
| 39 | msg string |
| 40 | errs []error |
| 41 | } |
| 42 | |
| 43 | func (e *joinedError) Error() string { return e.msg } |
| 44 | func (e *joinedError) Unwrap() []error { return e.errs } |
| 45 | |
| 46 | // Join combines multiple errors into one. Nil errors are filtered out. |
| 47 | // Returns nil if no non-nil errors are provided. |
| 48 | // Unlike [errors.Join], the returned error supports culpa formatting |
| 49 | // (fmt.Formatter, slog.LogValuer) and detail traversal. |
| 50 | func Join(errs ...error) error { |
| 51 | var filtered []error |
| 52 | for _, e := range errs { |
| 53 | if e != nil { |
| 54 | filtered = append(filtered, e) |
| 55 | } |
| 56 | } |
| 57 | switch len(filtered) { |
| 58 | case 0: |
| 59 | return nil |
| 60 | case 1: |
| 61 | return filtered[0] |
| 62 | default: |
| 63 | msgs := make([]string, len(filtered)) |
| 64 | for i, e := range filtered { |
| 65 | msgs[i] = e.Error() |
| 66 | } |
| 67 | return &joinedError{ |
| 68 | msg: strings.Join(msgs, "\n"), |
| 69 | errs: filtered, |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | // New creates a new error with a message. |
| 75 | func New(msg string) error { |
| 76 | return &culpaError{msg: msg} |
| 77 | } |
| 78 | |
| 79 | // Errorf creates a new error with a formatted message. |
| 80 | func Errorf(format string, args ...any) error { |
| 81 | return &culpaError{msg: fmt.Sprintf(format, args...)} |
| 82 | } |
| 83 | |
| 84 | // Wrap wraps an existing error with a message and captures a stacktrace. |
| 85 | // Returns nil if err is nil. |
| 86 | func Wrap(err error, msg string) error { |
| 87 | if err == nil { |
| 88 | return nil |
| 89 | } |
| 90 | return withStacktrace(&culpaError{msg: msg, cause: err}) |
| 91 | } |
| 92 | |
| 93 | // Wrapf wraps an existing error with a formatted message and captures |
| 94 | // a stacktrace. Returns nil if err is nil. |
| 95 | func Wrapf(err error, format string, args ...any) error { |
| 96 | if err == nil { |
| 97 | return nil |
| 98 | } |
| 99 | return withStacktrace(&culpaError{msg: fmt.Sprintf(format, args...), cause: err}) |
| 100 | } |
| 101 | |