Skip to content

Constructors

The seven functions that make an error. Behaviour below was taken from the package source and confirmed by running it.

New

func New(msg string) error

Returns a leaf error carrying msg and a stack captured at the call site.

KindOf on it reports errors.basic.

Not for a package-level sentinel

At package scope the call site is package initialisation, so the stack points at runtime.doInit and the var line rather than anywhere the error was returned from. Use NewSentinel.

Newf and Errorf

func Newf(format string, args ...any) error
func Errorf(format string, args ...any) error

The same function under two names — both spellings are in use across the estate. Formats the message and captures a stack.

%w is honoured, including more than once, because the message is built with fmt.Errorf rather than a reimplementation of it.

Note that the leaf produced this way has no kind of its own: KindOf returns "" for an error whose innermost layer came from Newf, and a slog record of it therefore carries no kind field. Give the leaf a NewSentinel identity if you need one.

NewSentinel

func NewSentinel(kind, msg string) error

Returns a package-level sentinel: no stack trace, and a stable kind string.

var ErrNotFound = errors.NewSentinel("forge.not_found", "provider not found")

The absence of a stack is the point — see the warning on New. Add a real one where you return it:

return errors.WithStack(ErrNotFound)

The kind is a serialisation key

Once a kind has crossed a process boundary, changing it breaks decoders that have not been updated. Treat it as you would a protobuf field number, and namespace it — forge.not_found, not not_found.

Wrap and Wrapf

func Wrap(err error, msg string) error
func Wrapf(err error, format string, args ...any) error

Prefixes err's message and captures a stack at the call site. The rendered message is msg + ": " + err.Error().

Both return nil when err is nil, so they need no guard:

return errors.Wrap(doLoad(name), "loading "+name)

Is and As continue to match through the wrapper, so wrapping a sentinel does not hide it:

err := errors.Wrap(ErrNotFound, "loading codeberg")
errors.Is(err, ErrNotFound)   // true

Join

func Join(errs ...error) error

Aggregates its non-nil arguments. Returns nil when none remain, so Join(nil, nil) and Join() are both nil.

Error() renders one member per line:

err := errors.Join(errors.New("first"), errors.New("second"))
fmt.Println(err)
first
second

No stack is attached to the aggregate. The joined errors carry their own, and adding one at this level is precisely what would break the shape described below.

Why this one behaves

Join satisfies Unwrap() []error directly — the aggregate is the outermost type, not something wrapped around it.

That matters because it is where cockroachdb/errors goes wrong: its Join wraps the aggregate in a stack-carrying layer, so the outermost type offers only a single unwrap, and every reporting traversal in that library then treats the aggregate as a leaf. Hints, details, context tags and telemetry keys all vanish below a Join (their issue #162, and it bites on the one-error path too, since their Join wraps even a single error).

Here, annotations below an aggregate stay readable:

err := errors.Join(
    errors.WithHint(errors.New("first"), "fix the first"),
    errors.WithAttrs(errors.New("second"), slog.Int("code", 42)),
)

errors.Hints(err)   // ["fix the first"]
errors.Attrs(err)   // [code=42]

KindOf on an aggregate reports errors.join, since that is the outermost kind that is not one of this package's annotation wrappers.