-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrors.go
More file actions
325 lines (293 loc) · 11.5 KB
/
Copy patherrors.go
File metadata and controls
325 lines (293 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
package errors
import (
stderrors "errors"
"log/slog"
"strings"
)
// Error is the one concrete error type of this package. It carries a human
// message, a [Severity], an optional machine-readable code, and an optional
// wrapped cause. It satisfies the standard error interface and exposes
// Unwrap so [errors.Is], [errors.As], and [errors.Unwrap] traverse the cause
// chain transparently.
//
// Construct an Error with [New] or [Wrap] rather than building the struct
// literal directly; the constructors apply the default severity and keep the
// invariants (notably the nil-cause short-circuit in [Wrap]) in one place.
// The fields are unexported on purpose: read them back through [SeverityOf]
// and [CodeOf], which walk the whole chain rather than inspecting a single
// layer.
type Error struct {
msg string
severity Severity
code string
cause error
// stack holds raw, unsymbolized program counters captured by [WithStack].
// nil unless WithStack was used. Symbolized lazily — see [stack.go].
stack []uintptr
// attrs are structured key/value pairs attached via [WithAttr]/[WithAttrs],
// surfaced through [AttrsOf] and the slog bridge ([Error.LogValue]). Typed
// as the stdlib's [slog.Attr] so they bridge to a structured logger for
// free, searchable rather than stringly encoded into the message.
attrs []slog.Attr
// hints are operator-facing remediation guidance attached via [WithHint],
// surfaced through [HintsOf]. They are NOT part of [Error.Error] — a hint
// tells a human what to DO, distinct from what went wrong.
hints []string
// public is the safe-to-surface message attached via [WithPublic]: the
// redacted, end-user-presentable summary that omits internal detail. The
// unexported msg/cause chain stays operator-only. Read via [PublicOf].
public string
// exitCode, when hasExit is true, is the process exit code this layer maps
// to via [WithExitCode]; surfaced through the [exitCodeCarrier] that
// [ExitCodeOf] looks for. A pointer-free presence flag keeps the zero value
// meaningful (exit code 0 is a legitimate, explicit choice — e.g. ErrHelp).
exitCode int
hasExit bool
// temporary / retryable opt this layer into the behaviour carriers
// [IsTemporary] / [IsRetryable] look for (Cheney's behaviour-over-type).
temporary bool
hasTemporary bool
retryable bool
hasRetryable bool
}
// Option configures an [Error] during [New] or [Wrap]. Options are applied in
// the order given, so a later option of the same kind overrides an earlier one.
type Option func(*Error)
// WithSeverity sets the [Severity] of the error being built. Without it, an
// error defaults to [DefaultSeverity] ([Error]).
func WithSeverity(s Severity) Option {
return func(e *Error) { e.severity = s }
}
// WithCode attaches a machine-readable code (for example "E_NOT_FOUND") to the
// error being built. Codes are free-form; [CodeOf] returns the first non-empty
// code found when walking a chain.
func WithCode(code string) Option {
return func(e *Error) { e.code = code }
}
// New creates a leaf [Error] with the given message and options. It has no
// wrapped cause, making it the idiomatic way to define sentinel errors:
//
// var ErrNotFound = errors.New("not found", errors.WithCode("E_NOT_FOUND"))
//
// The returned value's concrete type is *[Error]; the static return type is
// the standard error interface so callers compare and wrap it like any other
// error.
func New(msg string, opts ...Option) error {
e := &Error{msg: msg, severity: DefaultSeverity}
for _, opt := range opts {
opt(e)
}
return e
}
// Wrap adds a layer of context to err, returning a new [Error] whose cause is
// err. It mirrors the anyhow `.context(...)` pattern: the message describes
// what was being attempted, while the wrapped cause preserves the original
// error for [errors.Is] and [errors.As].
//
// Wrap returns nil when err is nil, so it is safe to call unconditionally in a
// tail position:
//
// return errors.Wrap(step(), "reconcile tenant config")
//
// If no [WithSeverity] option is given, the wrapper inherits err's severity
// (via [SeverityOf]) so context-only wrapping never silently downgrades a
// failure. Pass [WithSeverity] explicitly to re-classify.
func Wrap(err error, msg string, opts ...Option) error {
if err == nil {
return nil
}
e := &Error{msg: msg, severity: SeverityOf(err), cause: err}
for _, opt := range opts {
opt(e)
}
return e
}
// Error renders the message, prefixing wrapped causes in the conventional
// "context: cause" form so a fully wrapped chain reads as a single line from
// outermost intent to root cause.
func (e *Error) Error() string {
if e.cause == nil {
return e.msg
}
if e.msg == "" {
return e.cause.Error()
}
return e.msg + ": " + e.cause.Error()
}
// Unwrap returns the wrapped cause (or nil for a leaf error), letting
// [errors.Is], [errors.As], and [errors.Unwrap] traverse the chain.
func (e *Error) Unwrap() error { return e.cause }
// Severity reports this single layer's severity. Prefer the package-level
// [SeverityOf], which walks the whole chain; this method exists so an *Error
// can be inspected directly when you already hold one and to satisfy the
// severityCarrier interface that [SeverityOf] looks for.
func (e *Error) Severity() Severity { return e.severity }
// Code reports this single layer's code (which may be ""). Prefer the
// package-level [CodeOf], which walks the chain for the first non-empty code.
func (e *Error) Code() string { return e.code }
// severityCarrier is any error that knows its own [Severity]. Both [Error] and
// the [Join] aggregate implement it, and so may external error types that want
// to participate in [SeverityOf] without depending on this package's concrete
// type.
type severityCarrier interface {
error
Severity() Severity
}
// codeCarrier is any error that knows its own code. Both [Error] and the [Join]
// aggregate implement it.
type codeCarrier interface {
error
Code() string
}
// SeverityOf reports the severity of err by walking its cause chain and
// returning the severity of the outermost error that carries one. It returns
// [DefaultSeverity] ([SeverityError]) when err is nil or when nothing in the
// chain carries a severity, so any error — including a plain stdlib one — has a
// well-defined severity.
//
// Because [Wrap] inherits the wrapped error's severity by default, the value
// returned here is normally the most context-rich classification available:
// the outermost explicit annotation wins, and absent any, the original error's
// severity propagates out through every wrap.
func SeverityOf(err error) Severity {
var c severityCarrier
if stderrors.As(err, &c) {
return c.Severity()
}
return DefaultSeverity
}
// CodeOf reports the machine-readable code of err by walking its cause chain
// and returning the first non-empty code it finds, outermost first. It returns
// "" when err is nil, when nothing in the chain set a code, or when err neither
// is nor wraps a code-carrying error.
//
// Searching outermost-first means a wrapper may override an inner code by
// supplying its own via [WithCode], while context-only wraps transparently
// expose the inner error's code.
func CodeOf(err error) string {
for err != nil {
var c codeCarrier
if !stderrors.As(err, &c) {
return ""
}
if code := c.Code(); code != "" {
return code
}
// This carrier has no code of its own; descend past it to keep
// looking. errors.As found the outermost carrier, so unwrap from
// the value it matched rather than from the original err.
err = stderrors.Unwrap(c)
}
return ""
}
// Join aggregates several errors into one, mirroring [errors.Join]: nil inputs
// are dropped, the result is nil when every input is nil, and [errors.Is] /
// [errors.As] reach every joined member.
//
// The returned aggregate is itself inspectable by this package: its severity
// (via [SeverityOf]) is the most severe of its members, so joining a
// [SeverityNotice] with a [SeverityError] yields a [SeverityError]. Its
// [CodeOf] is the first non-empty code among its members, in argument order.
func Join(errs ...error) error {
var (
nonNil []error
sev Severity
hasSev bool
code string
exitCode int
hasExit bool
hints []string
attrs []slog.Attr
public string
)
for _, err := range errs {
if err == nil {
continue
}
nonNil = append(nonNil, err)
if s := SeverityOf(err); !hasSev || s > sev {
sev, hasSev = s, true
}
if code == "" {
code = CodeOf(err)
}
// Exit code of the aggregate is the most severe member's mapping: the
// numerically largest sysexits code among members, so a TEMPFAIL(75)
// member doesn't mask a SOFTWARE(70) sibling. First explicit wins ties.
if c := ExitCodeOf(err); c > exitCode {
exitCode, hasExit = c, true
} else if !hasExit && c != ExitOK {
exitCode, hasExit = c, true
}
hints = append(hints, HintsOf(err)...)
attrs = append(attrs, AttrsOf(err)...)
if public == "" {
public = PublicOf(err)
}
}
if len(nonNil) == 0 {
return nil
}
return &joinError{
errs: nonNil,
severity: sev,
code: code,
exitCode: exitCode,
hasExit: hasExit,
hints: hints,
attrs: attrs,
public: public,
}
}
// joinError is the aggregate produced by [Join]. It defers message rendering
// and chain traversal to the standard library's [errors.Join] result (held in
// joined) while layering this package's severity and code metadata on top via
// the Severity/Code methods that [SeverityOf]/[CodeOf] look for.
type joinError struct {
errs []error
severity Severity
code string
exitCode int
hasExit bool
hints []string
attrs []slog.Attr
public string
}
// Error renders the joined members one per line, matching [errors.Join]'s
// formatting so aggregates read the same regardless of which Join produced them.
func (j *joinError) Error() string {
var b strings.Builder
for i, err := range j.errs {
if i > 0 {
b.WriteByte('\n')
}
b.WriteString(err.Error())
}
return b.String()
}
// Unwrap returns the joined members, the multi-error form the standard library
// understands: [errors.Is] and [errors.As] fan out across every member.
func (j *joinError) Unwrap() []error { return j.errs }
// Severity lets [SeverityOf] report the aggregate's precomputed severity (the
// most severe member) without re-walking the tree.
func (j *joinError) Severity() Severity { return j.severity }
// Code lets [CodeOf] report the aggregate's precomputed code (the first
// non-empty member code) without re-walking the tree.
func (j *joinError) Code() string { return j.code }
// ExitCode lets [ExitCodeOf] report the aggregate's precomputed exit code (the
// most severe member's mapping). Reported only as an [exitCodeCarrier] when a
// member actually mapped one — see hasExitJoin.
func (j *joinError) ExitCode() int { return j.exitCode }
// hasExitJoin reports whether any member of the aggregate mapped an exit code,
// so [ExitCodeOf] can fall through to its severity default when none did.
func (j *joinError) hasExitJoin() bool { return j.hasExit }
// Hints lets [HintsOf] report the hints accumulated from every member without
// re-walking the multi-error tree (the single-Unwrap chain-walkers cannot
// descend into a []error aggregate).
func (j *joinError) Hints() []string { return j.hints }
// Attrs lets [AttrsOf] report the structured attrs accumulated from every
// member.
func (j *joinError) Attrs() []slog.Attr { return j.attrs }
// Public lets [PublicOf] report the first non-empty public message among
// members.
func (j *joinError) Public() string { return j.public }