errors.Join holds many errors in one¶
Level: 201 · anyone whose validator stops at the first problem when the user needed all of them
One line: errors.Join(errs...) returns one error whose Error() is their texts joined with newlines and whose Unwrap() []error lets errors.Is and errors.As search all of them; nils are dropped and a join of nothing but nils is nil, so a function can collect every problem and still return nil when there was none — and since Go 1.20 fmt.Errorf with two %w verbs wraps both operands the same way.
validate checks an order for three things — a customer, a known sku, a positive quantity — and appends an error for each one that fails, then returns errors.Join of the lot. main validates an order that fails all three, prints the joined error as text and as one quoted string, asks errors.Is and errors.As about it, and tries errors.Unwrap on it. Then it validates a good order, joins nothing, and builds an fmt.Errorf with two %w verbs.
Verified output of errors_join_holds_many_go.go — regenerated by tools/run_examples.py, never hand-typed.
Error() of the joined error:
customer is empty
unknown sku chair
quantity 0: quantity must be positive
as one quoted string: "customer is empty\nunknown sku chair\nquantity 0: quantity must be positive"
errors.Is(err, ErrNoCustomer) = true
errors.Is(err, ErrBadQuantity) = true
errors.As(err, &unknown) = true -> SKU: chair
errors.Unwrap(err) == nil = true
Unwrap() []error holds 3 errors
a valid order : <nil>
errors.Join() == nil : true
Join(nil, nil) == nil : true
two %w : order 12: customer is empty; quantity must be positive
errors.Is(both, ErrNoCustomer), errors.Is(both, ErrBadQuantity) = true true
errors.Unwrap(both) == nil = true
one %w : order 13: customer is empty
errors.Unwrap(one) = customer is empty
Reading the output¶
Error() is the texts joined with newlines. The quoted string shows the two \n. That is the whole of the formatting errors.Join ↗ promises: the Error of each element, a newline between each. A log line that expects one line gets three; What to do says what to do about that.
Is and As search every member. Both sentinels are found and errors.As fills in the UnknownSKUError, though each sits in a different member. The package overview describes the search as a tree walk — the error itself, then each child in turn, depth first (package errors ↗). The third member is itself a chain (quantity 0: %w), and ErrBadQuantity is found at the bottom of it.
errors.Unwrap returns nil on a joined error. errors.Unwrap(err) == nil = true. The documentation of Unwrap ↗ says it in so many words: it calls only a method of the form Unwrap() error, and does not unwrap errors returned by Join. To see the members, assert the Unwrap() []error interface yourself, as the program does to count 3 errors.
Nils vanish, and a join of nothing is nil. A valid order gives <nil>, and so do errors.Join() and errors.Join(nil, nil). That is what lets validate return errors.Join(problems...) unconditionally and still keep the err != nil contract for its callers.
Two %w verbs in one Errorf make the same shape. both prints as one line — Errorf formats, it does not add newlines — and errors.Is finds each operand; errors.Unwrap is nil on it too, because with more than one %w the returned error has Unwrap() []error (fmt.Errorf ↗). With one %w, one, it is the single chain of the previous lesson and errors.Unwrap returns the operand.
errors_join_holds_many_go.go
errors_join_holds_many_go.go in full — pasted here by tools/run_examples.py from the file CI runs.
// errors.Join holds several errors in one: Error() joins their texts with
// newlines, errors.Is and errors.As search all of them, and a Join of nothing
// but nils is nil. fmt.Errorf with two %w verbs wraps both operands the same
// way. Either kind has Unwrap() []error, which errors.Unwrap does not call.
//
// go build errors_join_holds_many_go.go && ./errors_join_holds_many_go
package main
import (
"errors"
"fmt"
)
var (
ErrNoCustomer = errors.New("customer is empty")
ErrBadQuantity = errors.New("quantity must be positive")
)
// UnknownSKUError names the sku the catalogue does not have.
type UnknownSKUError struct{ SKU string }
func (e UnknownSKUError) Error() string { return "unknown sku " + e.SKU }
type Order struct {
Customer string
SKU string
Quantity int
}
var catalogue = map[string]bool{"lamp": true, "desk": true}
// validate reports every problem with an order, not only the first one found.
func validate(o Order) error {
var problems []error
if o.Customer == "" {
problems = append(problems, ErrNoCustomer)
}
if !catalogue[o.SKU] {
problems = append(problems, UnknownSKUError{o.SKU})
}
if o.Quantity <= 0 {
problems = append(problems, fmt.Errorf("quantity %d: %w", o.Quantity, ErrBadQuantity))
}
return errors.Join(problems...)
}
func main() {
err := validate(Order{Customer: "", SKU: "chair", Quantity: 0})
fmt.Printf("Error() of the joined error:\n%v\n", err)
fmt.Printf("as one quoted string: %q\n", err.Error())
fmt.Println()
fmt.Println("errors.Is(err, ErrNoCustomer) =", errors.Is(err, ErrNoCustomer))
fmt.Println("errors.Is(err, ErrBadQuantity) =", errors.Is(err, ErrBadQuantity))
var unknown UnknownSKUError
fmt.Println("errors.As(err, &unknown) =", errors.As(err, &unknown), "-> SKU:", unknown.SKU)
fmt.Println("errors.Unwrap(err) == nil =", errors.Unwrap(err) == nil)
if multi, ok := err.(interface{ Unwrap() []error }); ok {
fmt.Printf("Unwrap() []error holds %d errors\n", len(multi.Unwrap()))
}
fmt.Println()
fmt.Println("a valid order :", validate(Order{"ada", "lamp", 2}))
fmt.Println("errors.Join() == nil :", errors.Join() == nil)
fmt.Println("Join(nil, nil) == nil :", errors.Join(nil, nil) == nil)
fmt.Println()
// fmt.Errorf with two %w verbs wraps both; with one it is the old single chain.
both := fmt.Errorf("order 12: %w; %w", ErrNoCustomer, ErrBadQuantity)
fmt.Println("two %w :", both)
fmt.Println("errors.Is(both, ErrNoCustomer), errors.Is(both, ErrBadQuantity) =", errors.Is(both, ErrNoCustomer), errors.Is(both, ErrBadQuantity))
fmt.Println("errors.Unwrap(both) == nil =", errors.Unwrap(both) == nil)
one := fmt.Errorf("order 13: %w", ErrNoCustomer)
fmt.Println("one %w :", one)
fmt.Println("errors.Unwrap(one) =", errors.Unwrap(one))
}
What to do¶
- Collect, then
Join, wherever several independent things can go wrong: a validator, a shutdown that closes several files, a batch where every item's failure matters. Returnerrors.Join(problems...)directly; it is already nil when the slice is empty. - Reach for
%wwhen the errors are one story andJoinwhen they are several.fmt.Errorf("order 12: %w", err)adds context to one failure;Joinsays these happened independently. - Remember that
errors.Unwrapdoes not see a joined error's members. Askerrors.Isorerrors.As, or assertinterface{ Unwrap() []error }. - Format a joined error with care where one line is expected. Its
Error()holds newlines; a log or a JSON field may want the members separately, whichUnwrap() []errorgives you. - With goroutines, decide first whether you want every error or the first one. The first error cancels the rest keeps the first and stops the others;
Joinis for the case where all of them must finish and every failure is reported.
Practice¶
Validate a whole batch of orders: each order's problems are joined, each failed order is wrapped with its position (order 2: …), and the batch error joins those. Print the batch error, count the failed orders through Unwrap() []error, ask errors.Is whether any order anywhere in the tree had no customer, and check that a batch with no problems returns nil.
Solution
errors_join_holds_many_kata_go.go in full — pasted here by tools/run_examples.py from the file CI runs.
// Kata: validate a whole batch. Each order's problems are joined, each failed
// order is wrapped with its position, and the batch error joins those. The
// caller counts failed orders through Unwrap() []error and asks errors.Is
// whether any order anywhere in the tree had no customer.
//
// go build errors_join_holds_many_kata_go.go && ./errors_join_holds_many_kata_go
package main
import (
"errors"
"fmt"
)
var (
ErrNoCustomer = errors.New("customer is empty")
ErrBadQuantity = errors.New("quantity must be positive")
)
type Order struct {
Customer string
Quantity int
}
func validate(o Order) error {
var problems []error
if o.Customer == "" {
problems = append(problems, ErrNoCustomer)
}
if o.Quantity <= 0 {
problems = append(problems, fmt.Errorf("quantity %d: %w", o.Quantity, ErrBadQuantity))
}
return errors.Join(problems...)
}
func validateBatch(orders []Order) error {
var failed []error
for i, o := range orders {
if err := validate(o); err != nil {
failed = append(failed, fmt.Errorf("order %d: %w", i+1, err))
}
}
return errors.Join(failed...)
}
func main() {
batch := []Order{{"ada", 2}, {"", 0}, {"grace", -1}, {"linus", 5}}
err := validateBatch(batch)
fmt.Println(err)
fmt.Println()
if multi, ok := err.(interface{ Unwrap() []error }); ok {
fmt.Println("failed orders:", len(multi.Unwrap()), "of", len(batch))
}
fmt.Println("some order had no customer:", errors.Is(err, ErrNoCustomer))
fmt.Println("all valid batch:", validateBatch([]Order{{"ada", 1}}))
}
Verified output of errors_join_holds_many_kata_go.go — regenerated by tools/run_examples.py, never hand-typed.
In other languages¶
- The Rust library's What makes a type an error ↗ — the standard library has no
Join: a validator returns its own type holding aVecof errors, with aDisplaythat lists them, and there is noerrors.Isto search it, so callers iterate theVec. - Python 3.11 added
ExceptionGroupandexcept*for the same need (theexceptionsmodule ↗) — a group is raised and matched by class, where a joined Go error is returned and searched witherrors.Is.
Sources¶
errors.Join↗,errors.Unwrap↗ and the package overview ↗ onUnwrap() []errorand the tree.fmt.Errorf↗ on more than one%w.- The Go 1.20 release notes ↗, which introduced both.
- The source:
src/errors/join.goat go1.25.5 ↗ andsrc/fmt/errors.go↗. - Jon Bodner, Learning Go, 2nd ed. (O'Reilly, 2024), chapter 9, "Errors" — the section "Wrapping Multiple Errors".
- Miki Tebeka, Effective Go Recipes (Pragmatic Bookshelf, 2024), chapter 8, "Working with Errors" — Recipe 46, "Wrapping Errors", which uses
errors.Jointo collect goroutine failures.