Getting started¶
By the end of this you will have created an error, attached the two things that make it useful to somebody else, and read them back out.
Install¶
Nothing else arrives with it. go.sum gains one line for this module and no
others.
Create and wrap¶
package main
import (
"fmt"
"gitlab.com/phpboyscout/go/errors"
)
func read(path string) error {
return errors.Newf("reading %s: not implemented", path)
}
func main() {
if err := read("config.yaml"); err != nil {
fmt.Printf("%v\n", err)
}
}
Wrap adds context as you come back up the stack, and returns nil for nil
so it composes without a guard:
Say what to do about it¶
An error message says what went wrong. A hint says what to do — and it is the half a user can act on:
return errors.WithHint(
errors.Wrap(err, "loading configuration"),
"Run `mytool init` to create a configuration file.",
)
The hint does not change the message. It travels alongside it:
fmt.Println(err) // loading configuration: reading config.yaml: …
fmt.Println(errors.Hints(err)) // [Run `mytool init` to create a configuration file.]
That separation is the point. The message goes wherever messages go; the hint is available to a CLI that wants to print guidance, and stays out of the way of one that does not.
Add something a machine can query¶
Hints are prose for a person. For anything you will later search on, attach
attributes — they are slog.Attr, so they land in a log record and a trace span
without translation:
import "log/slog"
return errors.WithAttrs(err,
slog.String("path", path),
slog.Int("attempt", attempt),
)
See all of it¶
%+v renders everything the error carries, most actionable first:
loading configuration: reading config.yaml: not implemented
HINT: Run `mytool init` to create a configuration file.
path=config.yaml
attempt=3
main.read
/home/you/project/main.go:11
main.load
/home/you/project/main.go:16
...
And a log handler gets it as structure rather than that block of text:
level=ERROR msg="startup failed" err.msg="loading configuration: …" \
err.kind=errors.attrs err.hint="[Run `mytool init` …]" err.path=config.yaml
One thing to get right from the start¶
A package-level sentinel should use NewSentinel, not New:
New captures a stack where it is called — and at package scope that is
initialisation, so the stack would point at runtime.doInit and the var line
rather than anywhere useful. NewSentinel records no stack; WithStack adds a
real one at the point you return it.
Declare a sentinel covers the pattern in full.