- Drop-in replacement for the standard
errorspackage – swap the import, keepNew,Is,AsandAsType. - Stack traces captured where the error is raised and printed with
%+v. - Fields – key-value context attached to an error.
- Cause – the error that caused this one, visible to
IsandAs. - Identity – every copy made by
With*is still the error it came from forerrors.Is. - Immutable – every
With*method returns a new error. - Structured logging – errors are logged by
slogas groups of their message, fields and cause. - Multi error – concurrent-safe collection of errors as a single
error, withJoinon top.
go get github.com/gravitton/errors- "errors"
+ "github.com/gravitton/errors"var ErrNotFound = errors.Sentinel("not found") // a plain error without a stack trace
err := errors.New("boom") // *Error with the current stack trace
err = errors.Newf("user %d: %w", 42, ErrNotFound)
err = errors.Wrap(io.EOF) // *Error around io.EOF
err = errors.Wrap(err) // an *Error is returned unchangedUse Sentinel, not New, for package-level errors: at package initialization, New captures a stack trace that
points nowhere useful. Wrap the sentinel where it is returned, which captures the stack trace there.
Check the error before wrapping it, never after: Wrap(nil) returns a nil *Error, which is not nil once stored in
an error. Reading a nil *Error or *MultiError is safe and prints <nil>, but With* and Add panic on one.
Wrap returns nil for one, and Add, Join and WithCause skip one, so it never ends up in a collection or as a
cause.
if err := load(); err != nil {
return errors.Wrap(err).WithField("file", name)
}
return nilerr := errors.Wrap(ErrNotFound).WithField("id", 42)
err = err.WithFields(map[string]any{"table": "users"})
err.Fields() // map[id:42 table:users]
errors.Is(err, ErrNotFound) // trueFields don't change identity: a copy made by With* is still the error it came from.
err := errors.New("timeout")
errors.Is(err.WithField("attempt", 3), err) // trueMatch a sentinel by the sentinel itself, not by a Wrap of it: an *Error target is only matched by an *Error in
the tree that wraps the sentinel or has it as a cause, never by a plain wrapper around the sentinel.
err := fmt.Errorf("load: %w", ErrNotFound)
errors.Is(err, ErrNotFound) // true
errors.Is(err, errors.Wrap(ErrNotFound)) // falseThe error that caused this one, or one that happened while handling it. Another cause is joined with the ones already
attached into one flat MultiError.
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
}A collection attaches a copy of its members, with nested collections flattened, and an empty one attaches nothing, so a collection can be attached as it is:
return errors.New("could not process batch").WithCause(errs)Wrapping an *Error, with Wrap or with Newf and %w, reuses its stack trace, which is closer to where the error
was raised. Only the Unwrap() error chain is searched: a collection, such as Join, gets the current stack trace.
The fields and cause of the wrapped error stay on it, reachable with errors.As:
outer := errors.Newf("load user: %w", err)
outer.StackTrace() // the stack trace of err
outer.Fields() // map[]
errors.Is(outer, ErrNotFound) // trueA MultiError collects errors into a single error. It is safe for concurrent use, so goroutines can add to one
collection without extra locking.
errs := errors.NewMulti()
errs.Add(process(1), process(2)) // nils, nil *Errors and nil *MultiErrors are skipped
return errs.ErrorOrNil() // nil when nothing was addedAn empty collection is kept when added, since errors may be added to it later, so the collection holding it is not empty even while it stays so:
child := errors.NewMulti()
errs.Add(child)
errs.ErrorOrNil() // not nil, with an empty messageerrs := 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, "errA\nerrB" as the standard errors.Join%v prints the message, %+v adds the fields, the stack trace and the cause, and %#v prints Go syntax without the
stack trace:
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 refused%+v applies to the underlying error and to every cause, so their details are printed too. An *Error wrapped with
%w is the exception: fmt prints only the wrapper's message, so its fields and cause are left out, reachable with
errors.As. A MultiError joins its members with newlines, as errors.Join does, with %+v applied to every member.
Wrapping a MultiError in an *Error prints its members first, so the fields and stack trace of the *Error follow
the last member at the same indentation.
logger.Error("load failed", "err", err)
// level=ERROR msg="load failed" err.msg="not found" err.fields.id=42 err.fields.table=users
logger.Error("batch failed", "err", errors.Join(err, io.EOF))
// level=ERROR msg="batch failed" err.0.msg="not found" err.0.fields.id=42 err.0.fields.table=users err.1=EOFThe stack trace is left out of logs; it belongs to %+v and error reporting.
StackTrace() []uintptr is the method Sentry looks for, so it picks up the stack trace; Frames() resolves it into
runtime.Frame values for other reporters. A reporter should walk the whole tree with Unwrap to collect the fields
of every *Error in it, and skip a stack trace equal to the one before, since wrapping errors share it.
A slog handler reporting errors gets the *Error from Value.Any() before resolving the value; once resolved, it is
the LogValue group. Handlers that resolve first, such as Sentry's, and ReplaceAttr hooks of the built-in handlers
find no error, so report the error where it is handled instead.
Full reference: pkg.go.dev.
errors.Unwrapreturnsnilfor every*Error, as it does forerrors.Join: it only followsUnwrap() error, and*ErrorimplementsUnwrap() []errorto expose both its underlying error and its cause. UseIsandAs, or walkUnwrap() []error.Newreturns*Error, soerr := errors.New("x")declares an*Errorthat can't be assigned a plainerrorlater.- Package-level
Newerrors report package initialization as their stack trace; turn them intoSentinels. Joinreturns a*MultiError, with the same message as the standard one.
The MIT License (MIT). Please see License File for more information.