What are sentinel errors, and what are their drawbacks?
Quick Answer
A sentinel error is a specific, pre-declared error value, like var ErrNotFound = errors.New("not found"), that callers check against directly (traditionally with ==, now idiomatically with errors.Is). They're simple and cheap to use for a small, fixed set of known conditions, like io.EOF or sql.ErrNoRows. The drawbacks: a sentinel error carries no additional data — it can tell you that something specific happened, but not which record, what value, or any other context, unless you wrap it with extra information separately. Sentinels also create an implicit API contract: once other packages check errors.Is(err, mypkg.ErrSomething), changing or removing that sentinel becomes a breaking change, similar to a public exported function.
Detailed Answer
Sentinel errors are the simplest error pattern in Go, and they're everywhere in the standard library, but they have real limits.
A typical sentinel
var ErrNotFound = errors.New("record not found")
func FindUser(id int) (*User, error) {
user, ok := db[id]
if !ok {
return nil, ErrNotFound
}
return user, nil
}
_, err := FindUser(42)
if errors.Is(err, ErrNotFound) {
fmt.Println("no such user")
}
The drawback: no extra context
if errors.Is(err, ErrNotFound) {
// You know SOMETHING wasn't found.
// You do NOT know which ID, which table, or any other detail,
// unless the caller happened to already have that information separately.
}
Compare this to a custom error type, which can carry the missing ID directly:
type NotFoundError struct{ ID int }
func (e *NotFoundError) Error() string { return fmt.Sprintf("id %d not found", e.ID) }
The drawback: an implicit public API contract
// package db
var ErrConnClosed = errors.New("connection closed")
Once external code does errors.Is(err, db.ErrConnClosed), that sentinel is effectively part of db's public API, exactly like an exported function. Renaming it, removing it, or changing when it's returned becomes a breaking change for every caller relying on it, even though it's "just a variable."
When sentinels are still the right choice
For a small, stable set of well-known conditions (think io.EOF, which will never need extra fields and will never change), a sentinel is simpler and perfectly idiomatic. Reach for a custom error type instead when callers need structured data out of the failure, not just a yes/no identity check.