What is errors.Join? How do multi-errors interact with errors.Is, errors.As and errors.Unwrap?
Question 299HardGo 1.22 to 1.25
errors.Join(errs ...error) (Go 1.20) returns an error that wraps every non-nil argument. It returns nil if all arguments are nil. Its message is the messages joined with newlines. It implements Unwrap() []error, and fmt.Errorf with several %w verbs produces the same shape.
var ErrA = errors.New("a")
var ErrB = errors.New("b")
err := errors.Join(ErrA, nil, fmt.Errorf("ctx: %w", ErrB))
fmt.Println(err) // a\nctx: b (two lines)
fmt.Println(errors.Is(err, ErrB)) // true: Is/As walk every branch
fmt.Println(errors.Unwrap(err)) // <nil> ← gotcha!
if u, ok := err.(interface{ Unwrap() []error }); ok {
for _, e := range u.Unwrap() {
fmt.Println("-", e)
}
}
fmt.Println(errors.Join(nil, nil) == nil) // true
Gotcha: errors.Unwrap only calls Unwrap() error, so it returns nil for joined errors. Use type assertion to walk them manually. The traversal is depth-first and pre-order, and errors.As returns the first match. Typical uses: validation that reports every bad field, cleanup that has to report both the main error and the Close error, and fan-out work.
More on Error Handling & panics
- Q297How does
errors.Iswork internally, and how can a custom type change its matching? - Q298Explain
errors.As. Why must the target be a pointer, and what happens if you get it wrong? - Q300Compare sentinel errors, typed errors and opaque errors. When would you use each?
- Q301How do you design a good custom error type? Does it matter whether
Error()has a pointer or value receiver? - Q302The nil error interface gotcha: what does this print?
- Q303When is
err == ErrXwrong, and why can comparing errors with==panic?