Learn Go← Dashboard

Mutex, RWMutex, WaitGroup, Once, atomic, errgroup.

errgroup — Concurrent Tasks with Shared Cancellation

golang.org/x/sync/errgroup is a third-party stdlib (semi-official: same team, separate module). It wraps the "launch N goroutines, wait for all, propagate the first error" pattern that otherwise needs a WaitGroup, a chan error, a sync.Once, and a context — all hand-wired.

If you write any real Go service, you will use errgroup.

Install

go get golang.org/x/sync/errgroup

It's outside the stdlib but maintained by the Go team and effectively standard.

The basic shape

import "golang.org/x/sync/errgroup"

var g errgroup.Group
for _, url := range urls {
    url := url   // (not needed in Go 1.22+, but harmless)
    g.Go(func() error {
        return fetch(url)
    })
}
if err := g.Wait(); err != nil {
    return err  // the first non-nil error from any goroutine
}

g.Go(fn) runs fn in a goroutine. g.Wait() blocks until all goroutines have finished and returns the first non-nil error.

With cancellation: errgroup.WithContext

The killer feature. If any goroutine returns an error, the shared context is cancelled — every other goroutine that respects the context bails out immediately:

go playground
Loading...

Worker 2 fails after 100 ms, ctx cancels, workers 1 and 3 see ctx.Done() and exit. g.Wait() returns worker 2's error. No worker is left dangling.

Limiting concurrency: SetLimit

For "do at most K concurrently":

g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8)              // at most 8 concurrent g.Go calls
for _, item := range items {
    item := item
    g.Go(func() error {
        return process(ctx, item)
    })
}
return g.Wait()

SetLimit makes g.Go block until a slot is free. A simple, sturdy worker pool with zero ceremony.

When NOT to use errgroup

  • You need every error, not just the first → collect into a []error and use errors.Join.
  • You need values back, not just errors → errgroup only returns errors; pair with a results channel or a typed group like sourcegraph/conc.
  • It's a single goroutine → errgroup is overkill; just call the function.
With `errgroup.WithContext`, what does `g.Wait()` return when goroutine A errors at t=10ms, goroutine B errors at t=20ms, and goroutine C succeeds?