Declare a sentinel¶
A sentinel is a package-level error value callers compare against with
errors.Is. Declare it with NewSentinel, not New.
package forge
import "gitlab.com/phpboyscout/go/errors"
var ErrProviderNotFound = errors.NewSentinel(
"forge.provider_not_found",
"no provider is registered for this source type",
)
Why not New¶
New captures a stack trace at the call site. At package scope the call site is
package initialisation, so the stack points at runtime.doInit and the
var line — never at anywhere the error was returned from.
That was harmless while nothing rendered stacks. It stops being harmless the moment stacks reach log records and trace spans, where an initialisation stack is worse than none: it looks like information.
NewSentinel records no stack at all.
Give it a real stack where you return it¶
func Lookup(sourceType string) (Provider, error) {
factory, ok := registry[sourceType]
if !ok {
return nil, errors.WithStack(ErrProviderNotFound)
}
return factory, nil
}
WithStack captures at the point of return, which is what a reader wants.
Wrap and Wrapf do the same as a side effect of adding a message, so you
rarely need WithStack explicitly:
Identity survives either way:
The kind string¶
The first argument is a stable identity used to route the error — to a wire codec, to a telemetry attribute. Namespace it with the package that owns it:
Treat it as you would a protobuf field number. Once it has crossed a process boundary, changing it breaks decoders that have not been updated.
It is also what will let a future decoder map a value that arrived over a
network back to this instance, so errors.Is keeps working on the far side —
pointer comparison does not survive a wire.