Skip to content

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:

return nil, errors.Wrapf(ErrProviderNotFound, "looking up %q", sourceType)

Identity survives either way:

errors.Is(err, forge.ErrProviderNotFound)   // true

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:

"forge.provider_not_found"      // good
"not_found"                     // collides with everyone

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.

Adding context without losing identity

err := errors.WithAttrs(
    errors.WithHint(
        errors.Wrapf(ErrProviderNotFound, "looking up %q", name),
        "Register one with forge.Register(), or check the spelling.",
    ),
    slog.String("source_type", name),
)

errors.Is(err, ErrProviderNotFound)   // still true