Skip to content

Hints, details and attributes

Three ways to attach something to an error, and the line between them is about audience, not format.

For Read by Shape
WithHint what to do about it the user, at the edge prose
WithDetail context worth reading an operator, in verbose output prose
WithAttrs context worth querying a machine, later slog.Attr

Hint: the actionable half

An error message says what went wrong. That is often not enough to act on, and the gap between "configuration uses a key that is no longer read" and "move it to auth.value" is the difference between a user solving their problem and filing an issue.

Hints are separate from the message so a library can offer guidance without forcing a caller to render it, and a caller can render it without parsing it out of a sentence.

They surface at the edge — a CLI printing to a terminal, a %+v dump. They are not part of Error().

Detail: prose for whoever is debugging

Detail is context that helps someone reading verbose output and does not belong in the message. It is the rarest of the three, and that is fine.

If you find yourself formatting a value into a detail string, you probably want an attribute instead.

Attributes: the part a query can reach

errors.WithAttrs(err,
    slog.String("host", host),
    slog.Int("attempt", attempt),
)

These are slog.Attr because both destinations want exactly that shape. A log record takes them directly; a trace span attribute is the same key/value pair. Flattening them into a string here would only mean parsing them back there.

Choosing the standard library's type rather than inventing one is what makes slog.LogValuer a pass-through and makes OpenTelemetry mapping somebody else's well-trodden problem rather than ours.

Why not one mechanism

It is tempting to have only attributes and derive the rest. It does not work: a hint is not a value with a key, it is a sentence aimed at a person, and hint="Run mytool init" as a queryable field is not useful to anyone.

The reverse fails too. Encoding host=codeberg.org into prose means every consumer that wants to filter by host writes a parser.

Two audiences, two shapes. The third — detail — exists because operator prose is genuinely neither, and pretending otherwise pushes people into misusing one of the other two.

Collection

Hints and details collect outermost-first and de-duplicate, because the layer closest to the user knows most about what they were trying to do, and the same advice arriving twice is noise.

Attributes collect outermost-first and do not de-duplicate. An outer layer recording a key an inner one also recorded is information — the same key at two levels of a call chain often has two different values — and deciding which wins is the consumer's call, not this package's.

A worked example

return 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),
)
  • the messagelooking up "codeberg": no provider is registered — is what gets logged, matched and wrapped
  • the hint is what a CLI prints when it gives up
  • the attribute is what you group by when you want to know which source types fail most