Documentation
¶
Overview ¶
Package errors is the phpboyscout estate's error package: stack traces, user-facing hints, structured attributes, and an aggregate that behaves.
It replaces github.com/cockroachdb/errors. See spec 0001 on this project's wiki for why, and for the decisions referenced throughout these files.
Nothing is imported ¶
This package depends on the standard library and nothing else (D1), which depfootprint_test.go asserts. That is the property being bought: an error package is imported by every module in the estate, so it should be the lightest thing in the graph rather than the heaviest.
The API is deliberately familiar ¶
Names and signatures match what the estate already called (D2), so migrating a module is an import-path change rather than a port.
Reading an error ¶
Everything that inspects an error — hints, attributes, stacks, formatting, slog rendering — goes through one traversal that handles single- and multi-unwrap alike (D3). Nothing type-switches on this package's own concrete types, so a layer it has never seen travels the chain intact. That is what makes a future wire codec's opaque degradation possible (D6), and it is the defect that made the previous library unusable for aggregates.
What lives elsewhere ¶
Wire serialization arrives as a sibling module keyed on ErrorKind (D6). OpenTelemetry belongs to gitlab.com/phpboyscout/go/observability, because deciding an error is worth recording happens where a span is in scope, not where the error is made (D7). This package ships no RecordError.
Index ¶
- Constants
- func As(err error, target any) bool
- func AsType[E error](err error) (E, bool)
- func Attrs(err error) []slog.Attr
- func Details(err error) []string
- func Errorf(format string, args ...any) error
- func FlattenDetails(err error) string
- func FlattenHints(err error) string
- func GetAllDetails(err error) []string
- func GetAllHints(err error) []string
- func Hints(err error) []string
- func Is(err, target error) bool
- func Join(errs ...error) error
- func KindOf(err error) string
- func New(msg string) error
- func NewSentinel(kind, msg string) error
- func Newf(format string, args ...any) error
- func Unwrap(err error) error
- func WithAttrs(err error, attrs ...slog.Attr) error
- func WithDetail(err error, detail string) error
- func WithDetailf(err error, format string, args ...any) error
- func WithHint(err error, hint string) error
- func WithHintf(err error, format string, args ...any) error
- func WithStack(err error) error
- func Wrap(err error, msg string) error
- func Wrapf(err error, format string, args ...any) error
- type Kinder
- type Payloader
- type StackTrace
- type StackTracer
- type StructuralKinder
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" )
Kinds this package defines. Exported because a wire codec registers against them (spec 0001 D6) and telemetry reports them (D7).
Variables ¶
This section is empty.
Functions ¶
func As ¶
As finds the first error in err's tree matching target's type.
Prefer AsType, which needs no out-parameter.
func AsType ¶
AsType finds the first error in err's tree of type E.
Delegates to the standard library, which gained this in Go 1.26. Requested of cockroachdb/errors as their #156 and unanswered; it is strictly better than As with a pointer argument, so it is here (D2).
func Attrs ¶
Attrs returns every structured attribute in err's tree, outermost first.
Duplicate keys are kept in order: an outer layer adding context that an inner one already recorded is information, not noise, and which wins is the caller's decision.
func FlattenDetails ¶
FlattenDetails concatenates every detail in err's tree.
func FlattenHints ¶
FlattenHints concatenates every hint in err's tree.
func GetAllDetails ¶
GetAllDetails is Details, under the name the estate already calls (D2).
func GetAllHints ¶
GetAllHints is Hints, under the name the estate already calls (D2).
func Join ¶
Join aggregates its non-nil arguments, returning nil when there are none.
Hints, details, attributes and stacks attached to the joined errors stay readable through this package's readers, because walk descends into the aggregate. Nothing is copied upward to make that work — the traversal is simply correct, which is what makes it impossible to regress in one reader and not another.
func KindOf ¶
KindOf returns an error's identity: the outermost kind that is not an annotation wrapper — this package's own, or one declared through StructuralKinder.
This is what belongs in a log record and in OTEL's exception.type. The outermost kind alone would report whichever annotation happened to be applied last — "errors.attrs" for an error that had attributes attached — which describes this package's plumbing rather than the failure.
Returns "" when nothing in the tree declares a kind.
func New ¶
New returns an error with a stack trace captured at the call site.
For a PACKAGE-LEVEL SENTINEL, use NewSentinel instead — see D11. At package scope the call site is initialisation, so the stack New captures points at runtime.doInit and the var declaration rather than anywhere the error was returned from. That is invisible today and stops being invisible as soon as stacks reach logs (D10) and spans (D7).
func NewSentinel ¶
NewSentinel returns a package-level sentinel: no stack trace, and a stable kind so its identity can be re-established after crossing a process boundary.
Two requirements meet here, which is the reason for one constructor rather than two. A sentinel must not carry an initialisation-time stack (D11); and Is compares pointers, which do not survive a wire, so a future codec needs a stable key to map a decoded leaf back to this instance (D6).
The kind is a serialisation key — see Kinder. Namespace it:
var ErrNotFound = errors.NewSentinel("forge.not_found", "provider not found")
func Newf ¶
Newf returns a formatted error with a stack trace. %w is honoured, including more than once.
func WithAttrs ¶
WithAttrs attaches structured context to an error.
slog.Attr is used rather than a type of our own because both consumers want exactly this shape: a log record (D10) and an OTEL span attribute (D7) are both key/value. Flattening to a string here would only mean parsing it back there. log/slog is standard library, so this costs no dependency (D1).
return errors.WithAttrs(err, slog.String("host", host), slog.Int("attempt", n))
Deliberately NOT a mutable map handed back to the caller, which is how tozd/go/errors does it: mutating an error after construction invites aliasing and races.
func WithDetail ¶
WithDetail attaches operator-facing detail — prose for a person.
For anything a machine will query, use WithAttrs instead (D12).
func WithDetailf ¶
WithDetailf attaches formatted operator-facing detail.
func WithStack ¶
WithStack annotates err with a call stack, unless the tree already carries one. Returns nil when err is nil.
This is how a sentinel from NewSentinel becomes a returnable error with a useful stack: the stack is captured where the error is RETURNED rather than where it was declared (D11).
return errors.WithStack(ErrNotFound)
Types ¶
type Kinder ¶
type Kinder interface {
ErrorKind() string
}
Kinder gives a layer a stable identity, used to route it — to a wire codec, 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, so treat these as you would a protobuf field number.
type Payloader ¶
Payloader exposes what a layer carries, for anything that needs to move or record it rather than print it.
The concrete type is per-kind and documented on each wrapper. A reader that does not recognise a kind must ignore the payload rather than assume a shape.
type StackTrace ¶
type StackTrace []uintptr
StackTrace is a captured call stack.
Exported, and renderable, because go/observability needs it for OTEL's exception.stacktrace (D7). A private field a formatter happened to reach would not serve that.
func DistinctStacks ¶ added in v0.2.0
func DistinctStacks(err error) []StackTrace
DistinctStacks returns the stacks in err's tree with the redundant ones dropped, outermost first.
Every capture happens at its own call site, so on a straight call path each outer stack is a strict suffix of the one captured deeper: wrapping three times in a row yields three stacks describing the same descent, and rendering all three repeats the same frames three times. Only the deepest carries anything the others do not.
A stack survives when it is not wholly contained in a deeper one, which is exactly when it says something new. Two cases produce that:
- an error created on one call path and wrapped from another, after being returned or stored
- an error wrapped in a different goroutine, where the stacks are disjoint
So the common case collapses to a single stack, the origin, and a genuinely divergent history keeps every branch of it.
func OriginStack ¶ added in v0.2.0
func OriginStack(err error) StackTrace
OriginStack returns the innermost stack trace in err's tree, or nil.
That is the one captured closest to where the error was created, which is what a debugger usually wants and what StackOf deliberately does not give. A wrapped error reports where it surfaced; this reports where it began.
func StackOf ¶
func StackOf(err error) StackTrace
StackOf returns the outermost stack trace in err's tree, or nil.
The outermost is the most recently captured, and so the closest to where the error surfaced — which is the one worth reporting. It is also why a sentinel from NewSentinel carrying none is harmless: WithStack at the return site supplies the useful one (D11).
func Stacks ¶ added in v0.2.0
func Stacks(err error) []StackTrace
Stacks returns every stack trace in err's tree, outermost first.
StackOf answers "where did this surface", which is the right question for a log line and is why it stops at the first stack it finds. This answers "how did it get here", which is the question a person debugging asks: each Wrap captures a stack at its own call site, so a chain wrapped three deep carries three, and the last one returned is the closest to where the error was made.
Nothing new is captured. The stacks were always in the tree; this reads all of them rather than the first.
The order matches the traversal, so it reads outward-in the same way the message chain does: "loading configuration: opening /etc/gtb: permission denied" pairs with surface, middle, origin.
func (StackTrace) String ¶
func (s StackTrace) String() string
String renders the conventional Go form — alternating function line and tab-indented file:line — which is what OTEL's exception.stacktrace expects and what a reader of a panic already knows how to scan.
type StackTracer ¶
type StackTracer interface {
StackTrace() StackTrace
}
StackTracer is implemented by any layer carrying a stack trace.
Separate from Kinder/Payloader on purpose: those route a layer to a codec or an attribute by kind, whereas a stack is a capability several kinds have — a leaf from New carries one, and so does a WithStack wrapper, though 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 (D3 rule 2).
type StructuralKinder ¶ added in v0.3.0
StructuralKinder lets a wrapper OUTSIDE this package declare that its kind describes how an error was annotated rather than what the error is, so that 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. Other packages wrap errors for the same reason and had no way to say so: go/errorhandling attaches an Outcome and an exit code, and their kinds were masking the identity of every error they touched.
Answering false is meaningful and is not the same as not implementing the interface at all: it says this kind IS an identity, deliberately.
func (w *withOutcome) ErrorKind() string { return KindOutcome }
func (w *withOutcome) StructuralKind() bool { return true }