# errgroup for Concurrent Tasks with Errors — Go Concurrency Patterns

Source: https://www.geekswithgeeks.com/en/go-concurrency/gc-errgroup

> Run tasks together, return the first error, cancel the rest.

## WaitGroup plus errors plus cancellation

`golang.org/x/sync/errgroup` provides `errgroup.Group`. `g.Go(func() error)` starts a task and `g.Wait()` blocks until all tasks return, then returns the **first non-nil error** (later errors are discarded). Created with `errgroup.WithContext(ctx)`, the group also returns a derived context that is **cancelled as soon as any task fails** (or when Wait returns), so siblings can stop early, provided they pass that context to their I/O. `SetLimit(n)` bounds concurrency and `TryGo` starts a task only if a slot is free. The zero `errgroup.Group{}` works too, without cancellation. If you need every error rather than the first, collect them yourself (for example into a slice by index) and combine with `errors.Join`.

## Failing well and stopping cleanly

Real services need to collect errors from concurrent work, respect rate limits and shut down without losing in-flight requests.

![Three ideas: errgroup, rate limiting and graceful shutdown.](assets/figures/go-concurrency/section-7-map.svg) — Figure 7.1: errors flowing back, requests throttled, shutdown draining work.

## Fetching a page of data in parallel

Results go into slots by index, so no mutex is needed.

```go
package main

import (
	"context"
	"fmt"

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

type User struct{ Name string }

func fetchUser(ctx context.Context, id int) (User, error) {
	if id == 3 {
		return User{}, fmt.Errorf("user %d not found", id)
	}
	return User{Name: fmt.Sprint("user-", id)}, nil
}

func fetchAll(ctx context.Context, ids []int) ([]User, error) {
	g, ctx := errgroup.WithContext(ctx)
	users := make([]User, len(ids)) // each goroutine writes only its own index
	for i, id := range ids {
		g.Go(func() error {
			u, err := fetchUser(ctx, id)
			if err != nil {
				return err // cancels ctx for the other tasks
			}
			users[i] = u
			return nil
		})
	}
	if err := g.Wait(); err != nil {
		return nil, err
	}
	return users, nil
}

func main() {
	_, err := fetchAll(context.Background(), []int{1, 2, 3, 4})
	fmt.Println(err) // user 3 not found
}
```

## Use the derived context, not the parent

Shadowing with `g, ctx := errgroup.WithContext(ctx)` makes it hard to accidentally pass the parent context to tasks, which would stop them noticing a sibling failure.

**Quiz:** What does g.Wait() return when two tasks in an errgroup fail?

- [ ] nil, because errors are only logged
- [ ] Both errors joined together
- [ ] The last error returned
- [x] The first non-nil error returned by any task

*Answer:* The first non-nil error returned by any task. errgroup keeps the first error; use errors.Join yourself if you need all of them.
