errgroup already cancels the context for you
Goconcurrencybackend
I used to hand-roll a context.WithCancel alongside an errgroup.Group, calling cancel() myself when one goroutine failed. Turns out errgroup.WithContext does exactly this, and I was duplicating it.
g, ctx := errgroup.WithContext(parent)
for _, url := range urls {
url := url
g.Go(func() error {
return fetch(ctx, url)
})
}
if err := g.Wait(); err != nil {
return err
}
The ctx it returns is cancelled the moment the first goroutine returns a non-nil error, or when Wait returns. Every other goroutine that respects the context stops on its own. No manual cancel, no leaked work chasing a result nobody wants anymore.
One caveat worth remembering: Wait returns only the first error. If you need all of them, collect into a slice guarded by a mutex, or reach for errors.Join after the fact. But for the common “fan out, fail fast” shape, WithContext is the whole pattern in one line.