TL;DR: An error message that only states what went wrong leaves the user stuck. go-tool-base moved its error handling from go-errors to cockroachdb/errors specifically to separate two things that had been jammed together: the error’s identity, which is for the program, and its hint, which is for the human. Hints carry the recovery step. And because the framework knows your tool’s support channel, a fatal error can end by telling the user exactly where to go for help.
A message is not a fix
Here’s an error a CLI tool might print:
error: failed to read config file
True, and useless. The user now knows something is wrong and has no idea what to do about it. Which file? Why couldn’t it be read? Should they create it, run an init, fix a permission, set an environment variable? The message states the problem and abandons them at it.
The instinct, when you notice this, is to write a better message:
error: failed to read config file at ~/.config/mytool/config.yaml.
Run 'mytool init' to create one, or set MYTOOL_CONFIG to point at an existing file.
Better for the human. But look at what you’ve just done to the error as a value. The recovery advice is now welded into the error string. Any code that wants to check “is this the config-missing error?” is reduced to substring-matching English prose. Reword the advice and you break the check. You’ve helped the user and sabotaged the program, because you made one string do two incompatible jobs: be a stable identity for code, and be friendly guidance for people.
Why go-tool-base changed error libraries
go-tool-base started on github.com/go-errors/errors. It’s a fine library and it gave us stack traces. What it didn’t give us was any way to attach human guidance to an error without putting it in the message string. So the codebase did exactly the bad thing above: multi-line suggestion text baked into errors.Errorf calls, user-facing content and programmatic identity mixed into one value.
That’s the reason for the migration to github.com/cockroachdb/errors. Not novelty. A specific capability: cockroachdb/errors lets you attach a hint to an error as a separate, structured field.
return errors.WithHint(
errors.New("failed to read config file"),
"Run 'mytool init' to create one, or set MYTOOL_CONFIG to point at an existing file.",
)
Now there are two things, cleanly apart. errors.New("failed to read config file") is the identity — stable, matchable, the program’s handle on the error. The hint is the guidance — for the human, and rewordable freely without breaking a single check, because no check looks at it. errors.Is and errors.As work properly through every wrapper layer, so code matches on identity and never has to read prose.
The migration brought other things worth having. Stack traces print with a plain %+v instead of a type assertion. Errors can carry structured, machine-readable metadata. Multiple errors from concurrent work can be combined as a first-class value. But the hint is the one that changed the user’s day, because the hint is the recovery step, stored where it belongs.
One door out, and it knows where the help is
Separating the hint is half of it. The other half is making sure hints actually reach the user, consistently, and that’s about having a single exit.
Every go-tool-base command returns its errors the idiomatic Cobra way, through RunE. They all funnel into one Execute() wrapper at the root, which routes every error — runtime failure, flag parse error, pre-run failure — through one ErrorHandler. One door out. So error presentation is decided in exactly one place, and no command can render an error differently from its neighbour.
And because there’s one handler, it can do something individual commands couldn’t. The framework knows your tool’s metadata, including its configured support channel — a Slack workspace, a Teams channel. So the error handler can end a fatal error not just with the what and the recovery hint, but with where to go if the hint didn’t help:
error: failed to read config file
hint: Run 'mytool init' to create one, or set MYTOOL_CONFIG.
Still stuck? Ask in #mytool-support on Slack.
The user is never left at a dead end. The error tells them what broke, the hint tells them the most likely fix, and if that’s not enough the handler tells them which door to knock on. A failure becomes a signpost instead of a full stop.
The bottom line
An error that only reports what went wrong leaves the user stranded, and the obvious fix — writing the recovery advice into the message — quietly wrecks the error as a value, because now code has to substring-match prose to identify it.
go-tool-base moved from go-errors to cockroachdb/errors to get hints: a structured, separate field for human guidance, leaving the error’s identity clean for errors.Is/errors.As. Every command’s errors exit through one Execute() wrapper and one ErrorHandler, so presentation is consistent, and because that handler knows the tool’s support channel it can point a stuck user at real help.
State the problem for the program. Give the fix to the human. Keep them in different fields.