Skip to content

Aggregate errors

Join collects several errors into one, and — unlike the library this replaces — does not lose what is attached to them.

var errs []error

for _, source := range sources {
    if err := check(source); err != nil {
        errs = append(errs, err)
    }
}

return errors.Join(errs...)

Join returns nil when every argument is nil, so the loop above needs no "were there any" branch.

Everything below it stays readable

a := errors.WithHint(errors.New("a failed"), "check A")
b := errors.WithHint(errors.New("b failed"), "check B")

joined := errors.Join(a, b)

errors.Hints(joined)   // [check A, check B]
errors.Is(joined, a)   // true
errors.StackOf(joined) // a's stack

This is worth stating because it is not true of cockroachdb/errors, where hints, details, context tags and telemetry keys all vanish below a Join — and that includes joining a single error, since it wraps even one.

Nothing is copied upward to make this work. There is one chain traversal in the package and it descends into an aggregate, so every reader gets the same answer.

It has the standard-library shape

multi, ok := joined.(interface{ Unwrap() []error })

That holds here. It does not for cockroachdb, whose Join wraps the aggregate in a stack wrapper — so the outermost value offers only a single Unwrap, and anything walking single-unwraps treats the whole aggregate as a leaf.

Ordering

Members are visited in the order given, depth-first through nested aggregates. Hints and details de-duplicate; attributes do not, because an outer layer recording a key an inner one also recorded is information, and which wins is your decision rather than this package's.

Aggregating without discarding a diagnosis

A common shape is "try several sources, report only if none worked". Keep the failures and return them if nothing succeeds:

func first(sources []Source) (Result, error) {
    var errs []error

    for _, s := range sources {
        result, err := s.Get()
        if err != nil {
            errs = append(errs, err)

            continue
        }

        return result, nil   // something worked: the failures were immaterial
    }

    return Result{}, errors.Join(errs...)
}

Because hints survive the join, a caller that gets nothing back still sees every suggestion the individual sources made.