How do you propagate errors from multiple goroutines? Explain errgroup.
Question 313MediumGo 1.22 to 1.25
Goroutines can't return values, so errors have to be passed back explicitly. golang.org/x/sync/errgroup is the standard tool. g.Go runs functions that return error. g.Wait blocks until all of them finish and returns the first non-nil error. WithContext cancels the derived context when the first error occurs, so the other goroutines can stop early. SetLimit bounds concurrency.
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8)
results := make([][]byte, len(urls))
for i, url := range urls { // Go 1.22: per-iteration i, url
g.Go(func() error {
b, err := fetch(ctx, url)
if err != nil {
return fmt.Errorf("fetch %s: %w", url, err)
}
results[i] = b // distinct index, no mutex needed
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Gotchas: each worker must actually respect ctx, otherwise cancellation does nothing. The context from WithContext is cancelled when Wait returns, so don't use it afterwards. Only the first error is kept. When you need all of them, collect them and use errors.Join. Go 1.25's sync.WaitGroup.Go makes launching goroutines easier, but it doesn't propagate errors.
More on Error Handling & panics
- Q311Which runtime failures can't be recovered with
recover? - Q312Compare
panic,os.Exit,log.Fatalandruntime.Goexitwith respect to deferred functions. - Q314How do you collect all errors from concurrent workers rather than just the first?
- Q315Go errors don't carry stack traces. How do you get stack information when you need it?
- Q316"Handle an error only once." What does that mean, and what is wrong with log-and-return?
- Q317Why is
defer f.Close()potentially a bug when writing files? How do you handle theCloseerror?