Joining Errors with errors.Join
Wrapping with %w builds a chain: A caused B caused C, one line of
descent. But some failures aren't a chain — they're siblings. A form with
three bad fields has three independent problems, and reporting only the first
is a poor experience. Since Go 1.20, errors.Join combines several errors into
one.
One error from many
err := errors.Join(err1, err2, err3)
The result is a single error value that holds all of them. Two rules make it
pleasant to use:
- nil arguments are skipped. You can pass a slice where some entries are
nilwithout filtering first. - all nil means nil. If every argument is
nil(or there are none),Joinreturnsnil— so "no problems" naturally becomes anilerror.
Its Error() string lists each joined error on its own line:
name is empty
age out of range
Is and As still work across the tree
This is the important part. A joined error isn't a chain, it's a small tree
of siblings — and errors.Is and errors.As search the whole tree, not just
a single spine:
err := errors.Join(ErrEmptyName, ErrBadAge)
errors.Is(err, ErrEmptyName) // true
errors.Is(err, ErrBadAge) // true
So a caller can test for each cause independently, exactly as if each had been returned on its own. Nothing about the combining hides the parts.
The killer use case: validate everything
Most validation is written fail-fast — return at the first problem. That makes
the user fix one thing, resubmit, discover the next. Collecting all the
failures is friendlier, and Join makes it easy:
func validate(name string, age int) error {
var errs []error
if name == "" {
errs = append(errs, ErrEmptyName)
}
if age < 0 || age > 150 {
errs = append(errs, ErrBadAge)
}
return errors.Join(errs...) // nil if errs is empty
}
One pass, every problem collected, and the caller still gets a nil error when
everything is fine — because Join on an empty (or all-nil) slice is nil.
Chain vs tree
Keep the two tools straight by what they model:
%wwrapping — a linear chain. Use it to add context as one error travels up: "loading config: open file: permission denied." Each layer explains the one below.errors.Join— a tree of siblings. Use it to report multiple independent failures at once: several invalid fields, several failed writes.
You can even mix them: join a batch of errors, and wrap the join with a summary
line. errors.Is still finds every leaf.
Why a tree
Go could have made a combined error an opaque blob, but then callers would be
back to parsing strings. Making it a tree that Is/As traverse keeps the
promise that errors are values you can inspect — even after you've bundled
several together, each cause stays individually addressable. Combine freely;
nothing is lost.
Try it
Write a validate that checks a name and an age and returns both failures at
once via errors.Join, so the tests can assert errors.Is matches each
sentinel independently.
Practice
1. Validate Everything at Once
IntermediateThe starter declares two sentinel errors and a validation function:
var ErrEmptyName = errors.New("name is empty") var ErrBadAge = errors.New("age out of range") func validate(name string, age int) errorImplement
validateso it reports all problems at once usingerrors.Join:- Include
ErrEmptyNamewhenname == "". - Include
ErrBadAgewhenage < 0 || age > 150. - When both are fine, return
nil.
The idiom is to collect the failures into a
[]errorand join them:var errs []error if name == "" { errs = append(errs, ErrEmptyName) } // ... check age ... return errors.Join(errs...)errors.Joinskipsnilarguments and returnsnilwhen there's nothing to report, so an all-valid input naturally yields anilerror. Crucially, the joined error is a tree thaterrors.Issearches: forvalidate("", -1)botherrors.Is(err, ErrEmptyName)anderrors.Is(err, ErrBadAge)are true, whilevalidate("", 30)matches onlyErrEmptyName.package main import ( "errors" "fmt" ) // ErrEmptyName is reported when the name is blank. var ErrEmptyName = errors.New("name is empty") // ErrBadAge is reported when the age is outside 0..150. var ErrBadAge = errors.New("age out of range") // validate checks name and age and returns ALL problems at once, combined with // errors.Join. Include ErrEmptyName when name == "", include ErrBadAge when // age < 0 || age > 150. When both are fine, return nil. func validate(name string, age int) error { // TODO: implement return nil } func main() { err := validate("", -1) fmt.Println(err) fmt.Println("empty name?", errors.Is(err, ErrEmptyName)) fmt.Println("bad age?", errors.Is(err, ErrBadAge)) fmt.Println(validate("Ada", 30)) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Include