Lesson 19 / 25
errgroup for Concurrent Tasks with Errors
Run tasks together, return the first error, cancel the rest.
WaitGroup plus errors plus cancellation
golang.org/x/sync/errgroup provides errgroup.Group. g.Go(func() error) starts a task and g.Wait() blocks until all tasks return, then returns the first non-nil error (later errors are discarded). Created with errgroup.WithContext(ctx), the group also returns a derived context that is cancelled as soon as any task fails (or when Wait returns), so siblings can stop early, provided they pass that context to their I/O. SetLimit(n) bounds concurrency and TryGo starts a task only if a slot is free. The zero errgroup.Group{} works too, without cancellation. If you need every error rather than the first, collect them yourself (for example into a slice by index) and combine with errors.Join.
Failing well and stopping cleanly
Real services need to collect errors from concurrent work, respect rate limits and shut down without losing in-flight requests.
Fetching a page of data in parallel
Results go into slots by index, so no mutex is needed.
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)
type User struct{ Name string }
func fetchUser(ctx context.Context, id int) (User, error) {
if id == 3 {
return User{}, fmt.Errorf("user %d not found", id)
}
return User{Name: fmt.Sprint("user-", id)}, nil
}
func fetchAll(ctx context.Context, ids []int) ([]User, error) {
g, ctx := errgroup.WithContext(ctx)
users := make([]User, len(ids)) // each goroutine writes only its own index
for i, id := range ids {
g.Go(func() error {
u, err := fetchUser(ctx, id)
if err != nil {
return err // cancels ctx for the other tasks
}
users[i] = u
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return users, nil
}
func main() {
_, err := fetchAll(context.Background(), []int{1, 2, 3, 4})
fmt.Println(err) // user 3 not found
}Use the derived context, not the parent
Shadowing with g, ctx := errgroup.WithContext(ctx) makes it hard to accidentally pass the parent context to tasks, which would stop them noticing a sibling failure.
Quick check: What does g.Wait() return when two tasks in an errgroup fail?
- nil, because errors are only logged
- Both errors joined together
- The last error returned
- The first non-nil error returned by any task
Answer
The first non-nil error returned by any task — errgroup keeps the first error; use errors.Join yourself if you need all of them.