🧗 Analogy
A climbing team is roped together on parallel pitches. The moment one climber falls (returns an error), the lead calls off the attempt and signals everyone to stop and come back — and reports what went wrong. That’s errgroup: wait for all, but abort on the first failure.
The problem
You run several things concurrently and any of them might fail. A sync.WaitGroup waits for them, but it doesn’t collect errors or stop the others when one breaks — you end up bolting on a shared error variable, a mutex, and a context. golang.org/x/sync/errgroup packages exactly that: wait + first-error + cancel-the-rest.
Structure
graph TD
G["errgroup.WithContext"] --> A["g.Go task A"]
G --> B["g.Go task B (fails)"]
G --> C["g.Go task C"]
B -. "first error cancels ctx" .-> A
B -. cancels ctx .-> C
G --> W["g.Wait → returns B's error"]
Idiomatic Go
g.Go launches each task; a failure cancels the shared ctx, and g.Wait returns the first error. Edit and Run:
package main
import (
"context"
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
g, ctx := errgroup.WithContext(context.Background())
jobs := []string{"alpha", "beta", "bad", "gamma"}
for _, job := range jobs {
job := job // capture per iteration
g.Go(func() error {
if job == "bad" {
return fmt.Errorf("job %q failed", job)
}
select {
case <-time.After(50 * time.Millisecond):
fmt.Println("done:", job)
return nil
case <-ctx.Done(): // a sibling failed → stop early
fmt.Println("cancelled:", job)
return ctx.Err()
}
})
}
if err := g.Wait(); err != nil {
fmt.Println("group failed with:", err)
}
}
🐹 The everyday concurrency workhorse
errgroup is the answer to “run these N things at once, stop if one fails.” Add g.SetLimit(n) and it becomes a bounded worker pool with error handling baked in — combining Worker Pool, Semaphore, and Context cancellation in a few lines. It lives in golang.org/x/sync, the semi-official extended standard library.
Pitfalls
⚠️ You get the FIRST error, not all of them
g.Wait returns only the first non-nil error; the rest are discarded (their goroutines are cancelled). If you need to see every failure, collect errors yourself (e.g. into a slice guarded by a mutex, or via errors.Join). Also: tasks must actually watch ctx.Done() — errgroup can signal cancellation, but a goroutine ignoring the context will run to completion regardless.
When to use it — and when not
✅ Reach for it when
- You fan work out concurrently and any task can fail — you want the first error and to stop the others.
- You'd otherwise hand-roll a WaitGroup plus error-collecting plus context cancellation.
- You want a built-in concurrency limit via SetLimit.
⛔ Think twice when
- Tasks never fail — a plain sync.WaitGroup is enough.
- You need *every* error, not just the first — collect them yourself.
Related patterns
Propagate cancellation, deadlines, and request-scoped values across API boundaries and goroutine trees with context.Context.
CONCURRENCY Worker PoolBound concurrency by feeding jobs to a fixed number of long-lived worker goroutines.
CONCURRENCY Fan-out / Fan-inDistribute work across multiple goroutines (fan-out) and merge their results back into one stream (fan-in).
CONCURRENCY SemaphoreLimit how many goroutines may run a section of code (or hold a resource) at the same time.
Check your understanding
Score: 0 / 31. What does errgroup add over sync.WaitGroup?
errgroup is a WaitGroup plus a shared error and (with WithContext) a context that cancels the rest when one goroutine returns an error.
2. What does errgroup.WithContext give you?
The derived ctx is cancelled on the first error (or when Wait returns), so the other goroutines selecting on ctx.Done() can stop early.
3. How do you bound concurrency with errgroup?
SetLimit(n) makes g.Go block until a slot is free, turning the group into a bounded worker pool with error handling.