diff --git a/README.md b/README.md index 40f09a1..9b5ae42 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,84 @@ and github.com/pkg/errors. It also adds a few lessons learned from creating a module like this a number of times across numerous projects. +## Familiar favorites at your disposal + +This pkg is a drop in replace for `github.com/pkg/errors`, with a nearly +identical interface. Similarly, the std lib `errors` module's functionality +have been replicated in this pkg so that you only ever have to work from +a single errors module. Here are some examples of what you can do: + +```go +package foo + +import ( + "github.com/jsteenb2/errors" +) + +func Simple() error { + return errors.New("simple error") +} + +func Enriched() error { + return errors.New("enriched error", errors.KVs("key_1", "some val", "power_level", 9000)) +} + +func ErrKindInvalid() error { + return errors.New("invalid kind error", errors.Kind("invalid")) +} + +func Wrapped() error { + // note if errors.Wrap is passed a nil error, then it returns a nil. + // matching the behavior of github.com/pkg/errors + return errors.Wrap(Simple()) +} + +func Unwrapped() error { + // no need to import multiple errors pkgs to get the std lib behavior. The + // small API surface area for the std lib errors are available from this module. + return errors.Unwrap(Wrapped()) // returns simple error again +} + +func WrapFields() error { + // Add an error Kind and some additional KV metadata. Enrich those errors, and better + // inform the oncall you that might wake up at 03:00 in the morning :upside_face: + return errors.Wrap(Enriched(), errors.Kind("some_err_kind"), errors.KVs("dodgers", "stink")) +} + +func Joined() error { + // defaults to printing joined/multi errors as hashicorp's go-multierr does. The std libs, + // formatter can also be provided. + return errors.Join(Simple(), Enriched(), ErrKindInvalid()) +} + +func Disjoined() []error { + // splits up the Joined errors back to their indivisible parts []error{Simple, Enriched, ErrKindInvalid} + return errors.Disjoin(Joined()) +} +``` + +This is a quick example of what's available. The std lib `errors`, `github.com/pkg/errors`, +hashicorp's `go-multierr`, and the `upspin` projects error handling all bring incredible +examples of error handling. However, they all have their limitations. + +The std lib errors are intensely simple. Great for a hot path, but not great for creating +structured/enriched errors that are useful when creating services and beyond. + +The `github.com/pkg/errors` laid the ground work for what is the std lib `errors` today, but +also provided access to a callstack for the errors. This module takes a similar approach to +`github.com/pkg/errors`'s callstack capture, except that it is not capturing the entire stack +all the time. We'll touch on this more soon. + +Now with the `go-multierr` module, we have excellent ways to combine errors into a single return +type that satisfies the `error` interface. However, similar to the std lib, that's about all +it does. You can use Is/As with it, which is great, but it does not provide any means to add +additional context or behavior. + +The best in show (imo, YMMV) for `error` modules is the `upspin` project's error handling. The +obvious downside to it, is its specific to `upspin`. For many applications creating this whole +error handling setup wholesale, can be daunting as the `upspin` project did an amazing job of +writing their `error` pkg to suit their needs. + ## Error behavior untangles the error handling hairball Instead of focusing on a multitude of specific error types or worse, @@ -20,8 +98,6 @@ error that exhibits a `not_found` behavior. package foo import ( - stderrors "errors" - "github.com/jsteenb2/errors" ) @@ -33,7 +109,7 @@ const ( func FooDo() { err := complexDoer() - if stderrors.Is(ErrKindNotFound, err) { + if errors.Is(ErrKindNotFound, err) { // handle not found error } } @@ -59,15 +135,16 @@ kinds above we can do something like: package foo import ( - stderrors "errors" "net/http" + + "github.com/jsteenb2/errors" ) func errHTTPStatus(err error) int { switch { - case stderrors.Is(ErrKindInvalid, err): + case errors.Is(ErrKindInvalid, err): return http.StatusBadRequest - case stderrors.Is(ErrKindNotFound, err): + case errors.Is(ErrKindNotFound, err): return http.StatusNotFound default: return http.StatusInternalServerError @@ -77,6 +154,84 @@ func errHTTPStatus(err error) int { Pretty neat yah? +## Adding metadata/fields to contextualize the error + +One of the strongest cases I can make for this module is the use of the `errors.Fields` +function we provide. Each error created, wrapped or joined, can have additional metadata +added to the error to contextualize the error. Instead of wrapping the error using +`fmt.Errorf("new context str: %w", err)`, you can use intelligent error handling, and +leave the message as refined as you like. Its simpler to just see the code in action: + +```go +package foo + +import ( + "github.com/jsteenb2/errors" +) + +func Up(timeline string, powerLvl, teamSize int) error { + return errors.New("the up failed", errors.Kind("went_ape"), errors.KVs( + "timeline", timeline, + "power_level", powerLvl, + "team_size", teamSize, + "rando_other_field", "dodgers stink", + )) +} + +func DownUp(timeline string, powerLvl, teamSize int) error { + // ... trim + err := Up(timeline, powerLvl, teamSize) + return errors.Wrap(err) +} +``` + +Here we are returning an error from the `Up` function, that has some context +added (via `errors.KVs` and `errors.Kind`). Additionally, we get a stack trace +added as well. Now lets see what these fields actually look like: + +```go +package foo + +import ( + "fmt" + + "github.com/jsteenb2/errors" +) + +func do() { + err := DownUp("dbz", 9009, 4) + if err != nil { + fmt.Printf("%#v\n", errors.Fields(err)) + /* + Outputs: []any{ + "timeline", "dbz", + "power_level", 9009, + "team_size", 4, + "rando_other_field", "dodgers stink", + "err_kind", "went_ape", + "stack_trace", []string{ + "github.com/jsteenb2/README.go:26[DownUp]", // the wrapping point + "github.com/jsteenb2/README.go:15[Up]", // the new error call + }, + } + + Note: the filename in hte stack trace is made up for this read me doc. In + reality, it'll show the file of the call to errors.{New|Wrap|Join}. + */ + } +} +``` + +The above output, is golden for informing logging infrastructure. Becomes very +simple to create as much context as possible to debug an error. It becomes +very easy to follow the advice of John Carmack, by adding assertions, or +good error handling in go's case, without having to drop a blender on the actual +error message. When that error message remains clean, it empowers your observability +stack. Reducing the cardinality and being able to see across the different facets +your fields provide can create opportunities to explore relationships between failures. +Additionally, there's a fair chance that a bunch of `DEBUG` logs can be removed. Your +SRE/infra teams will thank for it :-). + ## Limitations Worth noting here, this pkg has some limitations, like in