Skip to content

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

go get gitlab.com/phpboyscout/go/errors

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)
    }
}
reading config.yaml: not implemented

Wrap adds context as you come back up the stack, and returns nil for nil so it composes without a guard:

func load(path string) error {
    return errors.Wrap(read(path), "loading configuration")
}
loading configuration: reading config.yaml: not implemented

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:

fmt.Printf("%+v\n", err)
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:

slog.Default().Error("startup failed", "err", err)
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:

var ErrNotConfigured = errors.NewSentinel("mytool.not_configured", "not configured")

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.

Next