Lesson 5 · 5 of 20
Custom errors, Is, As and %w · Go
Create sentinel errors and your own error types in Go, wrap them with %w and recognise them later with errors.Is and errors.As.

ErrUnknown is a sentinel error: a package variable created with errors.New that stands for one specific failure, unknown product. Whoever receives it can ask whether their error is that one, without comparing strings.
StockError is a custom error type. Any type with an Error() string method satisfies the error interface, and this one also carries data: how many units were requested (Want) and how many there are (Have). The method is on the pointer, so the error returned is a *StockError.
reserve does not return those errors as they are: it wraps them with fmt.Errorf and the `%w` verb, which adds context (reserve: ...) and keeps the original error inside. The message becomes reserve: want 3, have 1, and the inner error is still available. With %v instead of %w only the text would be copied.
`errors.As(err, &se)` walks the chain of wrapped errors looking for a *StockError; if it finds one, it stores it in se and returns true, so you can compute se.Want - se.Have, 2. `errors.Is(err, ErrUnknown)` walks the same chain looking for that sentinel: true for XX-99, false for a nil error.
switch err := reserve(sku, 3); { declares err just for that switch and then evaluates each case as a condition. Remember: compare errors with errors.Is, not ==, because == cannot see inside a wrapped error.
Code · main.go
package mainimport ( "errors" "fmt")var ErrUnknown = errors.New("unknown product") // a sentinel errorvar stock = map[string]int{"KB-01": 5, "MS-02": 1}type StockError struct{ Want, Have int } // a custom error type that carries datafunc (e *StockError) Error() string { return fmt.Sprintf("want %d, have %d", e.Want, e.Have) }func reserve(sku string, qty int) error { if have, ok := stock[sku]; !ok { return fmt.Errorf("reserve: %w", ErrUnknown) // %w wraps the error } else if qty > have { return fmt.Errorf("reserve: %w", &StockError{qty, have}) } return nil}func main() { for _, sku := range []string{"KB-01", "MS-02", "XX-99"} { var se *StockError switch err := reserve(sku, 3); { case errors.As(err, &se): // is there a *StockError in the chain? fmt.Printf("%s: %v (short by %d)\n", sku, err, se.Want-se.Have) default: // errors.Is: is ErrUnknown anywhere in the chain? fmt.Printf("%s: %v (unknown: %t)\n", sku, err, errors.Is(err, ErrUnknown)) } }}Output · go run main.go
KB-01: <nil> (unknown: false) MS-02: reserve: want 3, have 1 (short by 2) XX-99: reserve: unknown product (unknown: true)
Key points
- A sentinel error is a variable created with
errors.New. - A type with
Error() stringis anerrorand can carry data. fmt.Errorf("...: %w", err)adds context without losing the original.errors.Islooks for a value in the chain;errors.As, for a type.- With wrapped errors,
==is not enough: useerrors.Is.
Exercise
Create an ErrInvalidQty sentinel and make reserve return it wrapped when qty <= 0. Add a case errors.Is(err, ErrInvalidQty): to the switch and try a quantity of 0.