@@ -7,6 +7,84 @@ and github.com/pkg/errors. It also adds a few lessons learned
7
7
from creating a module like this a number of times across numerous
8
8
projects.
9
9
10
+ ## Familiar favorites at your disposal
11
+
12
+ This pkg is a drop in replace for ` github.com/pkg/errors ` , with a nearly
13
+ identical interface. Similarly, the std lib ` errors ` module's functionality
14
+ have been replicated in this pkg so that you only ever have to work from
15
+ a single errors module. Here are some examples of what you can do:
16
+
17
+ ``` go
18
+ package foo
19
+
20
+ import (
21
+ " github.com/jsteenb2/errors"
22
+ )
23
+
24
+ func Simple () error {
25
+ return errors.New (" simple error" )
26
+ }
27
+
28
+ func Enriched () error {
29
+ return errors.New (" enriched error" , errors.KVs (" key_1" , " some val" , " power_level" , 9000 ))
30
+ }
31
+
32
+ func ErrKindInvalid () error {
33
+ return errors.New (" invalid kind error" , errors.Kind (" invalid" ))
34
+ }
35
+
36
+ func Wrapped () error {
37
+ // note if errors.Wrap is passed a nil error, then it returns a nil.
38
+ // matching the behavior of github.com/pkg/errors
39
+ return errors.Wrap (Simple ())
40
+ }
41
+
42
+ func Unwrapped () error {
43
+ // no need to import multiple errors pkgs to get the std lib behavior. The
44
+ // small API surface area for the std lib errors are available from this module.
45
+ return errors.Unwrap (Wrapped ()) // returns simple error again
46
+ }
47
+
48
+ func WrapFields () error {
49
+ // Add an error Kind and some additional KV metadata. Enrich those errors, and better
50
+ // inform the oncall you that might wake up at 03:00 in the morning :upside_face:
51
+ return errors.Wrap (Enriched (), errors.Kind (" some_err_kind" ), errors.KVs (" dodgers" , " stink" ))
52
+ }
53
+
54
+ func Joined () error {
55
+ // defaults to printing joined/multi errors as hashicorp's go-multierr does. The std libs,
56
+ // formatter can also be provided.
57
+ return errors.Join (Simple (), Enriched (), ErrKindInvalid ())
58
+ }
59
+
60
+ func Disjoined () []error {
61
+ // splits up the Joined errors back to their indivisible parts []error{Simple, Enriched, ErrKindInvalid}
62
+ return errors.Disjoin (Joined ())
63
+ }
64
+ ```
65
+
66
+ This is a quick example of what's available. The std lib ` errors ` , ` github.com/pkg/errors ` ,
67
+ hashicorp's ` go-multierr ` , and the ` upspin ` projects error handling all bring incredible
68
+ examples of error handling. However, they all have their limitations.
69
+
70
+ The std lib errors are intensely simple. Great for a hot path, but not great for creating
71
+ structured/enriched errors that are useful when creating services and beyond.
72
+
73
+ The ` github.com/pkg/errors ` laid the ground work for what is the std lib ` errors ` today, but
74
+ also provided access to a callstack for the errors. This module takes a similar approach to
75
+ ` github.com/pkg/errors ` 's callstack capture, except that it is not capturing the entire stack
76
+ all the time. We'll touch on this more soon.
77
+
78
+ Now with the ` go-multierr ` module, we have excellent ways to combine errors into a single return
79
+ type that satisfies the ` error ` interface. However, similar to the std lib, that's about all
80
+ it does. You can use Is/As with it, which is great, but it does not provide any means to add
81
+ additional context or behavior.
82
+
83
+ The best in show (imo, YMMV) for ` error ` modules is the ` upspin ` project's error handling. The
84
+ obvious downside to it, is its specific to ` upspin ` . For many applications creating this whole
85
+ error handling setup wholesale, can be daunting as the ` upspin ` project did an amazing job of
86
+ writing their ` error ` pkg to suit their needs.
87
+
10
88
## Error behavior untangles the error handling hairball
11
89
12
90
Instead of focusing on a multitude of specific error types or worse,
@@ -20,8 +98,6 @@ error that exhibits a `not_found` behavior.
20
98
package foo
21
99
22
100
import (
23
- stderrors " errors"
24
-
25
101
" github.com/jsteenb2/errors"
26
102
)
27
103
@@ -33,7 +109,7 @@ const (
33
109
34
110
func FooDo () {
35
111
err := complexDoer ()
36
- if stderrors .Is (ErrKindNotFound, err) {
112
+ if errors .Is (ErrKindNotFound, err) {
37
113
// handle not found error
38
114
}
39
115
}
@@ -59,15 +135,16 @@ kinds above we can do something like:
59
135
package foo
60
136
61
137
import (
62
- stderrors " errors"
63
138
" net/http"
139
+
140
+ " github.com/jsteenb2/errors"
64
141
)
65
142
66
143
func errHTTPStatus (err error ) int {
67
144
switch {
68
- case stderrors .Is (ErrKindInvalid, err):
145
+ case errors .Is (ErrKindInvalid, err):
69
146
return http.StatusBadRequest
70
- case stderrors .Is (ErrKindNotFound, err):
147
+ case errors .Is (ErrKindNotFound, err):
71
148
return http.StatusNotFound
72
149
default :
73
150
return http.StatusInternalServerError
@@ -77,6 +154,84 @@ func errHTTPStatus(err error) int {
77
154
78
155
Pretty neat yah?
79
156
157
+ ## Adding metadata/fields to contextualize the error
158
+
159
+ One of the strongest cases I can make for this module is the use of the ` errors.Fields `
160
+ function we provide. Each error created, wrapped or joined, can have additional metadata
161
+ added to the error to contextualize the error. Instead of wrapping the error using
162
+ ` fmt.Errorf("new context str: %w", err) ` , you can use intelligent error handling, and
163
+ leave the message as refined as you like. Its simpler to just see the code in action:
164
+
165
+ ``` go
166
+ package foo
167
+
168
+ import (
169
+ " github.com/jsteenb2/errors"
170
+ )
171
+
172
+ func Up (timeline string , powerLvl , teamSize int ) error {
173
+ return errors.New (" the up failed" , errors.Kind (" went_ape" ), errors.KVs (
174
+ " timeline" , timeline,
175
+ " power_level" , powerLvl,
176
+ " team_size" , teamSize,
177
+ " rando_other_field" , " dodgers stink" ,
178
+ ))
179
+ }
180
+
181
+ func DownUp (timeline string , powerLvl , teamSize int ) error {
182
+ // ... trim
183
+ err := Up (timeline, powerLvl, teamSize)
184
+ return errors.Wrap (err)
185
+ }
186
+ ```
187
+
188
+ Here we are returning an error from the ` Up ` function, that has some context
189
+ added (via ` errors.KVs ` and ` errors.Kind ` ). Additionally, we get a stack trace
190
+ added as well. Now lets see what these fields actually look like:
191
+
192
+ ``` go
193
+ package foo
194
+
195
+ import (
196
+ " fmt"
197
+
198
+ " github.com/jsteenb2/errors"
199
+ )
200
+
201
+ func do () {
202
+ err := DownUp (" dbz" , 9009 , 4 )
203
+ if err != nil {
204
+ fmt.Printf (" %#v \n " , errors.Fields (err))
205
+ /*
206
+ Outputs: []any{
207
+ "timeline", "dbz",
208
+ "power_level", 9009,
209
+ "team_size", 4,
210
+ "rando_other_field", "dodgers stink",
211
+ "err_kind", "went_ape",
212
+ "stack_trace", []string{
213
+ "github.com/jsteenb2/README.go:26[DownUp]", // the wrapping point
214
+ "github.com/jsteenb2/README.go:15[Up]", // the new error call
215
+ },
216
+ }
217
+
218
+ Note: the filename in hte stack trace is made up for this read me doc. In
219
+ reality, it'll show the file of the call to errors.{New|Wrap|Join}.
220
+ */
221
+ }
222
+ }
223
+ ```
224
+
225
+ The above output, is golden for informing logging infrastructure. Becomes very
226
+ simple to create as much context as possible to debug an error. It becomes
227
+ very easy to follow the advice of John Carmack, by adding assertions, or
228
+ good error handling in go's case, without having to drop a blender on the actual
229
+ error message. When that error message remains clean, it empowers your observability
230
+ stack. Reducing the cardinality and being able to see across the different facets
231
+ your fields provide can create opportunities to explore relationships between failures.
232
+ Additionally, there's a fair chance that a bunch of ` DEBUG ` logs can be removed. Your
233
+ SRE/infra teams will thank for it :-).
234
+
80
235
## Limitations
81
236
82
237
Worth noting here, this pkg has some limitations, like in
0 commit comments