While numerous error packages provide rich functionality, errutil is the minimal (opinionated) functionality GRAX needs for error traces.
Minimal functionality leads to:
- Consistent use through a codebase.
- A faster implementation.
- No assumptions to break. For example, when errors with meaning (UserNotFound) are offered.
With and friends produce an error containing location information. The locations in a chain of errors will surface in the Stack produced by BuildStack. The stack can then be logged, displayed, sent to a service etc.
The Wrap methods additionally wrap passed errors so errors.Is matches the original error. To understand when Wrap should be used instead of With, read the Whether to Wrap section of the Go 1.13 errors blog post.
Functions that do not expose Is/As errors as part of their contract, should look similar to:
funcaFunc() error {
...returnerrutil.With(err)
}Wrapping sentinel errors, should look similar to:
varErrNotFound=errors.New("not found")
...funcaFunc() error {
iferr:=bFunc(); err!=nil {
returnerrutil.Wrap(err, ErrNotFound)
}
...
}
iferr:=aFunc(); err!=nil {
iferrors.Is(err, ErrNotFound) {
// handle not found case
}
...
}Wrapping unknown errors (discouraged), should look similar to:
iferr:=aFunc(); err!=nil {
returnerrutil.Wrap(err)
}Wrapping custom errors that are not sentinels, should look similar to:
typeCustomErrorstruct {
Textstring
}
func (eCustomError) Error() string {
return"text: "+e.Text
}
// Must use a pointer to avoid accidental matches. Does not need to be the same type as CustomError.varErrCustom=errors.New("custom error")
func (CustomError) Is(targeterror) bool {
returntarget==ErrCustom
}
funcaFunc() error {
iferr:=bFunc(); err!=nil {
returnerrutil.Wrap(err, ErrCustom)
}
... }
iferr:=aFunc(); err!=nil {
varcErrCustomErroriferrors.As(err, &cErr) {
// use cErr.Text
}
...
}Custom errors should implement Baser or errors.Unwrap to maintain traces, similar to:
typeCustomErrorstruct {
ErrerrorTextstring
}
func (eCustomError) Error() string {
return"text: "+e.Text
}
func (eCustomError) Base() error {
returne.Err
}Simple logging could be done with:
iferr:=topOfCalls(); err!=nil {
log.Println(errutil.BuildStack(err))
}The tools/ directory is a separate Go module of static analyzers that enforce the usage patterns above.
Disclaimer: these analyzers were written largely with the assistance of LLM tooling. Their SSA-based dataflow reasoning is subtle, so review and test changes with care.
State: two analyzers (errwrap, errunchecked), SSA-based, distributed as standalone binaries under cmd/. Both report unused suppression directives as failures.
The rule summaries below are intentionally brief. The testdata/src/a/a.go file for each analyzer is the authoritative specification; every accepted and flagged case is a labeled example, and reviewing it directly is the most complete demonstration of the rules.
errwrap — testdata
Every returned error must carry errutil location info.
- unwrapped — a returned error that isn't wrapped (
return err,return errors.New(...), a bare field/map/index/call result). - new — wrapping
errors.New/fmt.Errorf; useerrutil.Newinstead.
Accepted (not flagged): nil, any value traced back to an errutil wrap, Base() error accessors, and struct fields whose every assignment is wrapped.
errunchecked — testdata
- unchecked —
errutil.With/Wrapapplied to a call result with no preceding nil check (e.g.return errutil.With(f())), which would wrap a possibly-nil error.
Accepted: the wrap sits behind a recognized nil check — err != nil, sentinel equality, errors.Is/As, comma-ok assertions, ctx.Err(), provably-non-nil bool predicates, or a nil-preserving helper.
Place //errutil:unwrapped, //errutil:new, or //errutil:unchecked on the statement (any line of a multiline call/return), the line above it, on the function, or before the package declaration (file-wide). Generated files (// Code generated ... DO NOT EDIT.) are not checked, though their functions still contribute non-nil facts to errunchecked.
go install github.com/graxinc/errutil/tools/errwrap/cmd/errwrap@latest
go install github.com/graxinc/errutil/tools/errunchecked/cmd/errunchecked@latest
errwrap ./...
errunchecked ./...- Garbage reduction. Currently we maintain pointer equality in the same vein as
errors.Newas developers likely expect, however it requires heap allocation. This only shows up however in very fast loops.