Sentinel Errors & errors.Is
Sentinel errors are package-level variables that represent a fixed failure identity.
Search across all documentation pages
Sentinel errors are package-level variables that represent a fixed failure identity.
Callers recognize them with errors.Is instead of string matching or brittle == checks that break once errors are wrapped.
Sentinel errors give your API a small vocabulary of stable outcomes: not found, already exists, canceled, invalid input.
Go 1.13 added errors.Is so those identities survive fmt.Errorf("...: %w", err) wrap chains.
Use sentinels for cross-package contracts; reach for custom types when callers need structured fields.
Quick-reference recipe card - copy-paste ready.
var ErrNotFound = errors.New("not found")
func Find(id string) (Item, error) {
if id == "" {
return Item{}, ErrNotFound
}
return Item{}, nil
}
if errors.Is(err, ErrNotFound) {
// handle missing resource
}When to reach for this:
io.EOF, os.ErrNotExist) apply to your domainpackage main
import (
"errors"
"fmt"
"os"
)
var ErrUserNotFound = errors.New("user not found")
func loadUser(path string) error {
_, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("load user %q: %w", path, ErrUserNotFound)
}
return fmt.Errorf("load user %q: %w", path, err)
}
return nil
}
func main() {
err := loadUser("alice.json")
switch {
case errors.Is(err, ErrUserNotFound):
fmt.Println("client: 404 user missing")
case err != nil:
fmt.Println("client: 500", err)
default:
fmt.Println("ok")
}
}What this demonstrates:
ErrUserNotFound) maps to a client outcomefmt.Errorf with %w preserves both os and domain sentinels in the chainerrors.Is finds ErrUserNotFound through wrapsos.IsNotExist is a convenience wrapper around errors.Is(err, os.ErrNotExist)errors.New returns an opaque error value with a fixed identity.%w implements Unwrap() error, linking outer and inner errors.errors.Is(err, target) returns true if err == target or any unwrap step matches.err == ErrX fails when a wrapper sits between caller and sentinel.| Pattern | Example | Guidance |
|---|---|---|
| Exported sentinel | var ErrNotFound = errors.New(...) | Part of public contract |
| Unexported sentinel | var errStale = errors.New(...) | Internal package only |
| Standard library | io.EOF, context.Canceled | Use errors.Is, not string contains |
io.EOF signals end of stream during Read, not necessarily a catastrophic failure.
Loop until errors.Is(err, io.EOF) after processing the final chunk.
// Bad: breaks after wrap
if err == ErrNotFound { ... }
// Good
if errors.Is(err, ErrNotFound) { ... }
// Also good for stdlib helpers
if os.IsNotExist(err) { ... }== after wrapping - Wrapped errors never equal the sentinel. Fix: Use errors.Is.ErrX variables become hard to document. Fix: Group related failures into a typed error or error code enum.strings.Contains - Fragile across wraps and wording changes. Fix: errors.Is or errors.As.return (*MyError)(nil) makes err != nil true. Fix: return nil or var err error = nil.errors.Is(err, io.EOF) separately from real errors.| Alternative | Use When | Don't Use When |
|---|---|---|
Custom struct + errors.As | Callers need fields (field name, retryable) | Only a yes/no branch is needed |
| Error codes in one type | Many related variants share shape | Each variant is truly independent |
| Opaque errors | Hiding implementation from callers | Callers must branch on specific causes |
fmt.Errorf without %w | Inner cause must not be inspectable | Callers need errors.Is through the chain |
A package-level var ErrX = errors.New("...") value with stable identity. Callers recognize it with errors.Is.
Wrapping creates a new error value whose direct comparison target is the wrapper, not the sentinel. errors.Is traverses Unwrap.
Export only errors that are part of your documented API. Keep internal failures unexported or opaque.
Use Err prefix and PascalCase: ErrNotFound, ErrConflict. Match Go standard library style.
Yes. errors.Is(err, io.EOF) is the idiomatic check in read loops and works through wraps.
os.IsNotExist is a helper that checks os.ErrNotExist (and wraps) on any error. errors.Is is the general mechanism.
Return sentinels (or typed errors) for contract outcomes; wrap with context using fmt.Errorf and %w at each layer.
If you need a table to explain them, consider a typed error with a code field or separate types per category.
Yes. errors.Is checks each error in a joined value (Go 1.20+).
When the wrapped error should not be visible to Is/As, such as sanitizing messages at a public API boundary.
It is an error value used for control flow at end of input. Handle it explicitly rather than logging it as a failure.
Map with errors.Is in handlers: not found to 404, conflict to 409. Keep mapping at the transport boundary.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026