Skip to content

Formatting and logging

What each verb prints, and what a slog handler receives.

Formatting is implemented once and reached by every wrapper — the wrappers hold no formatting logic of their own, so a layer this package has never seen prints correctly too.

The verbs

Given this error:

err := errors.WithAttrs(
    errors.WithDetail(
        errors.WithHint(
            errors.Wrap(ErrNotFound, "loading codeberg"),
            "Register it first.",
        ),
        "checked 3 registries",
    ),
    slog.String("host", "codeberg.org"),
)
Verb Output
%s loading codeberg: provider not found
%v loading codeberg: provider not found
%q "loading codeberg: provider not found"
%+v everything — see below

%s, %v and %q all render the message alone. Annotations never change the message; they travel alongside it.

%+v

loading codeberg: provider not found
HINT: Register it first.
DETAIL: checked 3 registries
host=codeberg.org
main.main
    /home/you/project/main.go:74
runtime.main
    /usr/local/go/src/runtime/proc.go:290
runtime.goexit
    /usr/local/go/src/runtime/asm_amd64.s:1771

The order is deliberate — remediation first, because it is the actionable part, then operator context, then the structured attributes, then the stack:

  1. the message
  2. every hint, each on its own line prefixed HINT:
  3. every detail, prefixed DETAIL:
  4. every attribute, as key=value
  5. the outermost stack

A section is absent entirely when the error carries nothing of that kind.

slog

Errors implement slog.LogValuer, so they render as a group rather than a flattened string:

logger.Error("release lookup failed", "err", err)
level=ERROR msg="release lookup failed" err.msg="loading codeberg: provider not found" \
  err.kind=forge.not_found err.hint="[Register it first.]" \
  err.detail="[checked 3 registries]" err.host=codeberg.org

The group contains:

Field Present when Value
msg always err.Error()
kind KindOf is non-empty the error's identity
hint there is at least one hint the list of hints
detail there is at least one detail the list of details
your keys attributes were attached each WithAttrs attribute, flattened into the group

Two consequences worth planning for:

kind is omitted when nothing declares one. An error whose leaf came from New, Newf or Errorf and which wraps no sentinel has no identity, so there is no kind field to query on. If you want to alert on a failure class, give the leaf a NewSentinel.

hint and detail are lists, and render with bracketshint="[Register it first.]" even for a single hint, because the value is a []string.

The stack is not in the log record

That is deliberate. It is large, it is rarely what a log line is for, and it remains available to a handler that wants it through StackOf — which is also how go/observability reaches it for a span.