- Drop-in replacement for the standard
errorspackage – swap the import, keepNew,Is,AsandAsType. - Fields – key-value context added at any layer, merged along the chain.
- Causes – previous errors attached with
WithCause, all visible toIsandAs. - Stack traces captured where the error is raised and printed with
%+v. - Immutable – every
With*method returns a new error. - Multi error – concurrent-safe collection of errors as a single
error, withJoinon top.
go get github.com/gravitton/errors- "errors"
+ "github.com/gravitton/errors"Creating and wrapping:
var ErrNotFound = errors.Sentinel("not found") // no stack trace, captured when derived or wrapped
err := errors.New("boom") // *Error with a stack trace
err = errors.Newf("user %d missing", 42)
err = errors.Wrap(io.EOF) // *Error with a stack trace around io.EOF
err = errors.Wrap(err) // an *Error with a stack trace is returned unchangedUse Sentinel, not New, for package-level errors: New captures the stack where it is called, and at package
initialisation that stack points nowhere useful. For the same reason, don't derive package-level errors with
WithField or WithCause.
Adding context at every layer keeps what was added before:
err := ErrNotFound.WithField("id", 42) // the stack trace is captured here
err = errors.Newf("load user: %w", err) // the stack trace is reused
err = errors.Wrap(fmt.Errorf("handler: %w", err)).WithField("handler", "users")
err.Fields() // map[handler:users id:42]
errors.Is(err, ErrNotFound) // trueCauses, the error that caused this one or one that happened while handling it:
if err := load(); err != nil {
return errors.New("could not load config").WithCause(err)
}if err := write(f); err != nil {
if closeErr := f.Close(); closeErr != nil {
return errors.Wrap(err).WithCause(closeErr)
}
return err
}Fields are data about one occurrence, not part of the error's identity:
errors.Is(err, ErrNotFound) // true
errors.Is(err, ErrNotFound.WithField("id", 7)) // true, fields are not compared
errors.Is(err, errors.New("not found")) // false, a different error with the same textCheck the error before wrapping it, never after: Wrap(nil) returns a nil *Error, which is not nil once stored in
an error.
func Process() error {
if err := subProcess(); err != nil {
return errors.Wrap(err).WithField("process", "abc")
}
return nil
}Collecting errors, sequentially or concurrently:
errs := errors.NewMulti()
errs.Add(process(1), process(2)) // nils are skipped
return errs.ErrorOrNil() // nil when nothing was addederrs := errors.NewMulti()
wg := sync.WaitGroup{}
for i := range 10 {
wg.Go(func() {
if err := process(i); err != nil {
errs.Add(errors.Wrap(err).WithField("process", i))
}
})
}
wg.Wait()
return errs.ErrorOrNil()errors.Join(nil, nil) // nil
errors.Join(errA, errB) // *MultiError, "2 errors occurred:\n\terrA\n\terrB"Printing, the message alone with %v, or the fields, the stack trace and the causes with %+v:
fmt.Printf("%+v", err)
// process failed
// process=abc
// main.Process
// /app/main.go:14
// caused by: could not dial
// main.dial
// /app/main.go:27
// caused by: connection refusedEach cause is indented one level, so the cause of a cause is told apart from its siblings.
%+v only reaches the fields and the stack trace when the outermost error is an *Error: fmt.Errorf does not
implement fmt.Formatter, so fmt.Errorf("handler: %w", err) prints the message alone. Wrap it, or use Newf.
Full reference: pkg.go.dev.
- Standard library:
Unwrap,Is,As,AsTypeandErrUnsupportedare re-exported.errors.Unwrapreturnsnilfor an*Error, since it unwraps to its underlying error and its causes; useIsandAs. - Wrapping:
Wrap,NewfandFieldslook for inner*Errorvalues through single-error wrappers only, never through causes or errors wrapping several errors, such as aMultiError. The first stack trace found is reused, otherwise up to 32 frames are captured. - Sentry:
StackTracereturns the program counters under the name Sentry looks for, so it picks them up.
The MIT License (MIT). Please see License File for more information.