Compare sentinel errors, typed errors and opaque errors. When would you use each?
Question 300MediumGo 1.22 to 1.25
- Sentinel (
var ErrNotFound = errors.New(...)): a fixed value checked witherrors.Is. It is simple and cheap, but it can't carry context, and it becomes part of your public API, which couples callers to your package. Good for a small set of well-known conditions (io.EOF,sql.ErrNoRows). - Typed (
type ValidationError struct{Field string}): carries structured data, checked witherrors.As. More expressive, but callers must import the type, which increases coupling. - Opaque: callers only check
err != nil, or check for a behavior through an interface (interface{ Timeout() bool }), without knowing the concrete type. This gives the least coupling and was popularized by Dave Cheney's "assert errors for behaviour, not type".
var ErrNotFound = errors.New("store: not found") // sentinel
type ValidationError struct{ Field, Reason string } // typed
func (e *ValidationError) Error() string { return e.Field + ": " + e.Reason }
func IsRetryable(err error) bool { // behavior (opaque)
var r interface{ Retryable() bool }
return errors.As(err, &r) && r.Retryable()
}
Senior answer: export as few error identities as possible, and treat every exported sentinel or type as a compatibility commitment. Translate lower-layer errors at package boundaries.
More on Error Handling & panics
- Q298Explain
errors.As. Why must the target be a pointer, and what happens if you get it wrong? - Q299What is
errors.Join? How do multi-errors interact witherrors.Is,errors.Asanderrors.Unwrap? - Q301How do you design a good custom error type? Does it matter whether
Error()has a pointer or value receiver? - Q302The nil error interface gotcha: what does this print?
- Q303When is
err == ErrXwrong, and why can comparing errors with==panic? - Q304When should you wrap an error and when should you not? How do you handle errors at package boundaries?