Skip to content

Commit bd2574f

Browse files
committed
add ReasonableError type
1 parent 26a7f1c commit bd2574f

File tree

1 file changed

+131
-0
lines changed

1 file changed

+131
-0
lines changed

pkg/errors/reasonable_error.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package errors
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
var _ ReasonableError = &ErrorWithReason{}
10+
11+
// ReasonableError enhances an error with a reason.
12+
// The reason is meant to be a CamelCased, machine-readable, enum-like string.
13+
// Use WithReason(err, reason) to wrap a normal error into an ReasonableError.
14+
type ReasonableError interface {
15+
error
16+
Reason() string
17+
}
18+
19+
// ErrorWithReason wraps an error and adds a reason to it.
20+
// The reason is meant to be a CamelCased, machine-readable, enum-like string.
21+
// Use WithReason(err, reason) to wrap a normal error into an *ErrorWithReason.
22+
type ErrorWithReason struct {
23+
error
24+
reason string
25+
}
26+
27+
// Reason returns the reason for this error.
28+
func (e *ErrorWithReason) Reason() string {
29+
return e.reason
30+
}
31+
32+
// WithReason wraps an error together with a reason into ErrorWithReason.
33+
// The reason is meant to be a CamelCased, machine-readable, enum-like string.
34+
// If the given error is nil, nil is returned.
35+
func WithReason(err error, reason string) ReasonableError {
36+
if err == nil {
37+
return nil
38+
}
39+
return &ErrorWithReason{
40+
error: err,
41+
reason: reason,
42+
}
43+
}
44+
45+
// Errorf works similarly to fmt.Errorf, with the exception that it requires an ErrorWithReason as second argument and returns nil if that one is nil.
46+
// Otherwise, it calls fmt.Errorf to construct an error and wraps it in an ErrorWithReason, using the reason from the given error.
47+
// This is useful for expanding the error message without losing the reason.
48+
func Errorf(format string, err ReasonableError, a ...any) ReasonableError {
49+
if err == nil {
50+
return nil
51+
}
52+
return WithReason(fmt.Errorf(format, a...), err.Reason())
53+
}
54+
55+
// Join joins multiple errors into a single one.
56+
// Returns nil if all given errors are nil.
57+
// This is equivalent to NewErrorList(errs...).Aggregate().
58+
func Join(errs ...error) ReasonableError {
59+
return NewReasonableErrorList(errs...).Aggregate()
60+
}
61+
62+
// ReasonableErrorList is a helper struct for situations in which multiple errors (with or without reasons) should be returned as a single one.
63+
type ReasonableErrorList struct {
64+
Errs []error
65+
Reasons []string
66+
}
67+
68+
// NewReasonableErrorList creates a new *ErrorListWithReasons containing the provided errors.
69+
func NewReasonableErrorList(errs ...error) *ReasonableErrorList {
70+
res := &ReasonableErrorList{
71+
Errs: []error{},
72+
Reasons: []string{},
73+
}
74+
return res.Append(errs...)
75+
}
76+
77+
// Aggregate aggregates all errors in the list into a single ErrorWithReason.
78+
// Returns nil if the list is either nil or empty.
79+
// If the list contains a single error, that error is returned.
80+
// Otherwise, a new error is constructed by appending all contained errors' messages.
81+
// The reason in the returned error is the first reason that was added to the list,
82+
// or the empty string if none of the contained errors was an ErrorWithReason.
83+
func (el *ReasonableErrorList) Aggregate() ReasonableError {
84+
if el == nil || len(el.Errs) == 0 {
85+
return nil
86+
}
87+
reason := ""
88+
if len(el.Reasons) > 0 {
89+
reason = el.Reasons[0]
90+
}
91+
if len(el.Errs) == 1 {
92+
if ewr, ok := el.Errs[0].(ReasonableError); ok {
93+
return ewr
94+
}
95+
return WithReason(el.Errs[0], reason)
96+
}
97+
sb := strings.Builder{}
98+
sb.WriteString("multiple errors occurred:")
99+
for _, e := range el.Errs {
100+
sb.WriteString("\n")
101+
sb.WriteString(e.Error())
102+
}
103+
return WithReason(errors.New(sb.String()), reason)
104+
}
105+
106+
// Append appends all given errors to the ErrorListWithReasons.
107+
// This modifies the receiver object.
108+
// If a given error is of type ErrorWithReason, its reason is added to the list of reasons.
109+
// nil pointers in the arguments are ignored.
110+
// Returns the receiver for chaining.
111+
func (el *ReasonableErrorList) Append(errs ...error) *ReasonableErrorList {
112+
for _, e := range errs {
113+
if e != nil {
114+
el.Errs = append(el.Errs, e)
115+
if ewr, ok := e.(ReasonableError); ok {
116+
el.Reasons = append(el.Reasons, ewr.Reason())
117+
}
118+
}
119+
}
120+
return el
121+
}
122+
123+
// Reason returns the first reason from the list of reasons contained in this error list.
124+
// If the list is nil or no reasons are contained, the empty string is returned.
125+
// This is equivalent to el.Aggregate().Reason(), except that it also works for an empty error list.
126+
func (el *ReasonableErrorList) Reason() string {
127+
if el == nil || len(el.Reasons) == 0 {
128+
return ""
129+
}
130+
return el.Reasons[0]
131+
}

0 commit comments

Comments
 (0)