Skip to content

Commit 4be1dc0

Browse files
authored
Merge pull request #7 from jsteenb2/chore/readme
chore: extend README with examples of usage
2 parents ac41a4e + 48dcbfa commit 4be1dc0

File tree

1 file changed

+161
-6
lines changed

1 file changed

+161
-6
lines changed

README.md

Lines changed: 161 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,84 @@ and github.com/pkg/errors. It also adds a few lessons learned
77
from creating a module like this a number of times across numerous
88
projects.
99

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+
1088
## Error behavior untangles the error handling hairball
1189

1290
Instead of focusing on a multitude of specific error types or worse,
@@ -20,8 +98,6 @@ error that exhibits a `not_found` behavior.
2098
package foo
2199

22100
import (
23-
stderrors "errors"
24-
25101
"github.com/jsteenb2/errors"
26102
)
27103

@@ -33,7 +109,7 @@ const (
33109

34110
func FooDo() {
35111
err := complexDoer()
36-
if stderrors.Is(ErrKindNotFound, err) {
112+
if errors.Is(ErrKindNotFound, err) {
37113
// handle not found error
38114
}
39115
}
@@ -59,15 +135,16 @@ kinds above we can do something like:
59135
package foo
60136

61137
import (
62-
stderrors "errors"
63138
"net/http"
139+
140+
"github.com/jsteenb2/errors"
64141
)
65142

66143
func errHTTPStatus(err error) int {
67144
switch {
68-
case stderrors.Is(ErrKindInvalid, err):
145+
case errors.Is(ErrKindInvalid, err):
69146
return http.StatusBadRequest
70-
case stderrors.Is(ErrKindNotFound, err):
147+
case errors.Is(ErrKindNotFound, err):
71148
return http.StatusNotFound
72149
default:
73150
return http.StatusInternalServerError
@@ -77,6 +154,84 @@ func errHTTPStatus(err error) int {
77154

78155
Pretty neat yah?
79156

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+
80235
## Limitations
81236

82237
Worth noting here, this pkg has some limitations, like in

0 commit comments

Comments
 (0)