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¶
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.
StructuralKinder¶
Declares that a wrapper's kind describes how an error was annotated rather
than what the error is, so KindOf looks past it.
This package skips its own annotations already — a hint or a stack says how an error was annotated, a sentinel's kind says what it is. Packages outside it wrap errors for the same reasons and previously had no way to say so, which meant their wrappers masked the identity of every error they touched.
func (w *withOutcome) ErrorKind() string { return KindOutcome }
func (w *withOutcome) StructuralKind() bool { return true }
Implement it on the wrapper type. Answering false is meaningful — it says the
kind is an identity — and saying nothing at all keeps the previous behaviour,
so nothing changes for a wrapper that does not opt in.
Payloader¶
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¶
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:
Newcaptures a stack, so the leaf reportserrors.basic.NewSentinelcaptures no stack and is given an explicit kind, so the leaf reports that kind — noterrors.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¶
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:
That is what OTEL expects, and what a reader of a Go panic already knows how to scan.