Go

How does errgroup.WithContext use context, and what is a common bug with it?

Question 282HardGo 1.22 to 1.25

errgroup.WithContext(parent) (from golang.org/x/sync/errgroup) returns a group and a derived ctx. That ctx is canceled the first time a goroutine returns a non-nil error, or when Wait returns, whichever happens first. The error is recorded as the context's cause, and Wait returns the first error. SetLimit(n) bounds concurrency.

func fetchAll(ctx context.Context, urls []string) ([][]byte, error) {
	g, ctx := errgroup.WithContext(ctx)
	g.SetLimit(8)
	out := make([][]byte, len(urls))
	for i, u := range urls { // Go 1.22: i and u are per-iteration
		g.Go(func() error {
			b, err := fetch(ctx, u) // siblings abort once one fails
			if err != nil {
				return fmt.Errorf("%s: %w", u, err)
			}
			out[i] = b // distinct index per goroutine: no race
			return nil
		})
	}
	if err := g.Wait(); err != nil {
		return nil, err
	}
	return out, nil
}

Common bugs:

  • Shadowing and reusing ctx after Wait(). The line g, ctx := errgroup.WithContext(ctx) replaces the outer ctx, and that derived context is always canceled once Wait returns. Any later call in the same function that uses ctx fails with "context canceled". Name it gctx, or keep the group in a separate function.
  • Goroutines that ignore ctx. Cancellation then does nothing, and Wait still blocks until the slowest goroutine finishes.

More on Context

All 35 Context questions