When a Go project grows, sentinel errors multiply. Every package defines its own ErrNotFound, ErrInvalidInput, ErrPermissionDenied. The presentation layer ends up with long switch statements mapping each error to HTTP status codes:
switch {
case errors.Is(err, user.ErrNotFound):
return http.StatusNotFound
case errors.Is(err, order.ErrNotFound):
return http.StatusNotFound
case errors.Is(err, product.ErrNotFound):
return http.StatusNotFound
// ... and so on
}
I wanted a simpler approach: define error categories once and check against them. So I built knownerror.
How it works
Create category errors and extend them:
// Categories (usually in a shared package)
var (
ErrNotFound = errors.New("not found")
ErrForbidden = errors.New("forbidden")
)
// Domain-specific errors extend categories
var ErrUserNotFound = knownerror.New("user not found").Extends(ErrNotFound)
var ErrOrderNotFound = knownerror.New("order not found").Extends(ErrNotFound)
Now a single check handles all “not found” cases:
if errors.Is(err, ErrNotFound) {
return http.StatusNotFound
}
Attaching root cause
When returning a sentinel error, you often want to preserve the underlying cause for logging:
user, err := db.QueryUser(id)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound.WithCause(err)
}
The returned error still matches ErrUserNotFound via errors.Is(), but errors.Unwrap() gives you the original database error.
Multiple categories
An error can extend multiple categories:
var ErrResourceUnavailable = knownerror.New("resource unavailable").
Extends(ErrNotFound, ErrForbidden)
// Both checks return true:
errors.Is(err, ErrNotFound) // true
errors.Is(err, ErrForbidden) // true
When to use
Most projects don’t need this. A handful of errors per package is fine to handle individually. Consider this library when you have many sentinel errors across packages and want centralized categorization for presentation layer mapping.
Repository: github.com/pprishchepa/knownerror