Skip to content

Kinds and interfaces

The three interfaces a layer can implement, the eight kind constants this package defines, and the one exported type.

Nothing in this package type-switches on its own concrete types. Every reader reaches a layer through the interfaces below, which is what lets a layer the package has never seen — a decoded wire layer, or a consumer's own error type — travel the chain intact and contribute correctly.

Kinder

type Kinder interface {
    ErrorKind() string
}

Gives a layer a stable identity, used to route it: to a wire codec, or to a telemetry attribute.

The string is a serialisation key

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

Payloader

type Payloader interface {
    ErrorKind() string
    ErrorPayload() any
}

Exposes what a layer carries, for anything that needs to move or record it rather than print it.

The concrete payload type is per-kind — see the table below. A reader that does not recognise a kind must ignore the payload rather than assume a shape.

StackTracer

type StackTracer interface {
    StackTrace() StackTrace
}

Implemented by any layer carrying a stack.

It is separate from Kinder/Payloader on purpose: those route a layer by kind, whereas a stack is a capability several kinds have. A leaf from New carries one and so does a WithStack wrapper, but they are different kinds with different payloads.

Implementing it is how a consumer's own error type, or a decoded wire layer, contributes a stack to StackOf.

The kind constants

const (
    KindBasic    = "errors.basic"
    KindSentinel = "errors.sentinel"
    KindMessage  = "errors.message"
    KindStack    = "errors.stack"
    KindHint     = "errors.hint"
    KindDetail   = "errors.detail"
    KindAttrs    = "errors.attrs"
    KindJoin     = "errors.join"
)

They are exported because a wire codec registers against them and telemetry reports them.

Constant Value Payload type Structural?
KindBasic errors.basic string — the message no
KindSentinel errors.sentinel string — the message no
KindMessage errors.message string — the prefix yes
KindStack errors.stack StackTrace yes
KindHint errors.hint string yes
KindDetail errors.detail string yes
KindAttrs errors.attrs []slog.Attr yes
KindJoin errors.join []error no

Structural means the kind describes how an error was annotated rather than what it is, and so is skipped by KindOf when working out an error's identity.

Where basic and sentinel come from

Both are produced by the same internal leaf type, and which kind it reports depends on how it was made:

  • New captures a stack, so the leaf reports errors.basic.
  • NewSentinel captures no stack and is given an explicit kind, so the leaf reports that kind — not errors.sentinel.

errors.sentinel is therefore what a stackless leaf reports when it was given no kind of its own — which happens when you pass an empty kind:

errors.KindOf(errors.NewSentinel("forge.x", "m"))   // "forge.x"
errors.KindOf(errors.NewSentinel("", "m"))          // "errors.sentinel"
errors.KindOf(errors.New("m"))                      // "errors.basic"

An empty kind gives up the property the constructor exists for, since errors.sentinel is shared by every such error and cannot identify yours after a wire crossing. Namespace it.

StackTrace

type StackTrace []uintptr

func (s StackTrace) String() string

A captured call stack, bounded at 32 frames.

It is exported and renderable because go/observability needs it for OTEL's exception.stacktrace — a private field that a formatter happened to reach would not serve that.

String() renders the conventional Go form, alternating a function line with a tab-indented file:line:

main.load
    /home/you/project/main.go:16
main.main
    /home/you/project/main.go:24

That is what OTEL expects, and what a reader of a Go panic already knows how to scan.