Go

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 with errors.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 with errors.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

All 37 Error Handling & panics questions