Error values in depth
Sentinels, custom types, wrapping, and handling each error once.
- Write a sentinel error and a custom error type, and wrap them with
%w. - Write checks with
errors.Isfor a specific error anderrors.Asfor its data. - Explain the difference between wrapping with
%wand with%v.
What is an error in Go?
An error is any value with an Error() string method: error is just a one-method interface. Now that you know what an interface is, error stops being magic. It is this, and nothing more:
type error interface { Error() string}The four kinds of error you will write
// 1. Ad-hoc — a message, wrapping what went wrong underneathfunc parseLine(n int, err error) error { return fmt.Errorf("parsing config line %d: %w", n, err)}// 2. Sentinel — the caller needs to branch on this exact casevar ErrNotFound = errors.New("not found")// 3. Custom type — the caller needs DATA out of the errortype ValidationError struct{ Field, Msg string }func (e *ValidationError) Error() string { return e.Field + ": " + e.Msg }// 4. Joined — report every failure, not just the firstfunc validate(errs []error) error { return errors.Join(errs...) // nil if errs is empty}func handle(err error) { if errors.Is(err, ErrNotFound) { // is this exact error anywhere in the chain? fmt.Println("not found") } var ve *ValidationError if errors.As(err, &ve) { // is there a *ValidationError in the chain? give it to me fmt.Println("bad field:", ve.Field) }}What is the difference between %w and %v?
%wkeeps the original error in a chain, soerrors.Isanderrors.Ascan still see it. This is the default choice.%vflattens it to text and breaks the chain. Use it on purpose when you want to *hide* an internal error from the caller.- Add context, do not restate. Six layers each saying "failed to" gives you a useless sentence. Say what you were doing, with what:
fmt.Errorf("get user %d: %w", id, err). - Handle an error once. Either log it or return it, not both. Otherwise one failure passing up five layers is logged five times.
How do you get data out of an error with errors.As?
Pass errors.As a pointer to a variable of your error type; it finds that type in the wrap chain and fills the variable. errors.Is answers "is this that exact error?". When the error is a custom type carrying data, you want the value itself. errors.As walks the wrap chain looking for an error of the type you ask for. If it finds one, it stores it in your variable and returns true.
You pass a pointer to a variable of the error type: var ve *ValidationError and then errors.As(err, &ve). Forget the & and it panics, because it has nowhere to write the result.
package mainimport ( "errors" "fmt")type ValidationError struct{ Field, Msg string }func (e *ValidationError) Error() string { return e.Field + ": " + e.Msg }func saveUser(age int) error { if age < 0 { return &ValidationError{Field: "age", Msg: "cannot be negative"} } return nil}func main() { err := fmt.Errorf("signup: %w", saveUser(-3)) // wrapped once var ve *ValidationError if errors.As(err, &ve) { // found a *ValidationError in the chain fmt.Println("tell the user to fix:", ve.Field) // age } fmt.Println("full message:", err) // signup: age: cannot be negative}erroris just an interface with one method. Nothing about it is magic.errors.Isfor "which error is this",errors.Asfor "give me its data".%wkeeps the chain soIsandAscan see through it.%vbreaks it, which is sometimes what you want.- Handle an error once: log it or return it, not both.
%w, and retrieve them with errors.Is and errors.As.Price(prices map[string]int, item string) (int, error). A known item with a price of 0 or more returns that price and nil. A missing item returns an error that wraps ErrNotFound and reads price of "kiwi": not found. A negative price returns a *BadPriceError so the caller can get the item and price back with errors.As. Example: Price(map[string]int{"apple": 30}, "apple") → 30, nil.Tab indents. Press Esc, then Tab, to leave the editor.
- 1Wrap an error three layers deep and print the whole chain.
- 2Swap one
%wfor%vand watcherrors.Isstop finding it. - 3Use
errors.Jointo report three validation failures at once.