Concurrency pattern · Intermediate

errgroup

Run a group of goroutines, wait for them all, capture the first error, and cancel the rest automatically.

Concurrency Intermediate Complete

🧗 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.

Check your understanding

Score: 0 / 3

1. 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.