DMarlon Studio

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.

Go · Custom errors, Is, As and %w in DMarlon Studio: the example's code and its output in the terminal

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() string is an error and can carry data.
  • fmt.Errorf("...: %w", err) adds context without losing the original.
  • errors.Is looks for a value in the chain; errors.As, for a type.
  • With wrapped errors, == is not enough: use errors.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.