Skip to content
Muhammet Şafak
tr
Asked by: Onur Answered:

Should I run several dependent operations with errgroup so the first error cancels the rest?


Question

In an API we write in Go, at the start of every request we make five parallel HTTP calls to separate upstream services (user, subscription, campaign, inventory and pricing). Right now I fan them out with a plain `sync.WaitGroup` and collect all the results. The problem: the moment one of these five fails, there's no point waiting for the other four, because I'll finish the request with an error anyway. Should I move to `errgroup` to abort the whole batch on the first error, or is that overkill?

Answer

Short answer: yes, move to it.

Short answer

This is precisely the scenario errgroup.WithContext exists to solve; it’s not overkill here, it’s the right tool. sync.WaitGroup gives you “wait for all”; what you want is “if one falls, drop the rest”. errgroup is a small, mature dependency living under golang.org/x/sync/errgroup, and it’s well worth pulling in for this kind of fan-out.

Why

  1. errgroup.WithContext is the idiom. WithContext hands the group a derived context and cancels it the moment any goroutine returns a non-nil error. So the first error automatically sends a “stop” signal to the remaining calls; you don’t have to build any signalling yourself.

  2. Cancellation is cooperative — your calls must honor ctx. This is the most commonly missed point. Even if ctx is cancelled, if your HTTP call doesn’t carry that context via http.NewRequestWithContext, cancellation has zero effect; the call keeps running until it finishes. Same goes for DB queries. If you don’t thread the context all the way down, half of errgroup’s value is wasted.

    g, ctx := errgroup.WithContext(ctx)
    results := make([]Result, len(ids))
    for i, id := range ids {
        i, id := i, id // loop-var capture, pre-Go 1.22
        g.Go(func() error {
            r, err := lookup(ctx, id) // ctx flows into http.NewRequestWithContext inside
            if err != nil {
                return fmt.Errorf("lookup %s: %w", id, err)
            }
            results[i] = r
            return nil
        })
    }
    if err := g.Wait(); err != nil {
        return nil, err // the FIRST error only
    }
  3. g.Wait() returns only the first error. The other goroutines’ errors are dropped. If you need to collect the errors from all five calls, errgroup is the wrong tool; you’d want WaitGroup + a channel or errors.Join. But since your need is “first one’s enough, cancel the rest”, it fits exactly.

  4. Wait() still blocks until all goroutines return. Cancellation says “stop”, but Wait keeps blocking until each goroutine has actually returned. If you have a call that ignores ctx, you’ll still wait as long as the slowest one — which is why point 2 is critical.

What to do

  1. Don’t write shared results without synchronization. If each goroutine writes to its own results[i] index it’s safe (separate memory cells). Write to a shared map and you get a data race — caught by -race. Use slice indices or a mutex.

  2. SetLimit if the fan-out grows. Unbounded concurrency is fine for five calls, but if that number grows, cap concurrent goroutines with g.SetLimit(n); don’t hammer your upstreams.

  3. Put a deadline on the parent context. Cancellation only fires if one of the calls returns an error. If none error but one hangs (a slow upstream, an unresponsive socket), errgroup waits forever. Guard against that by wrapping the context you pass to WithContext with context.WithTimeout; when the deadline hits, ctx is cancelled, every call that honors ctx returns, and your request finishes within a bounded time.

Bottom line: personally I’d switch to errgroup.WithContext, put a request-level timeout on top of it, thread ctx down into every HTTP/DB call, and write results into a per-index slice. On Go 1.22+ you can even drop the loop-var capture line. In short: set up both guarantees together — “drop the rest if one falls” and “time out if nobody returns”.

Related Reading

Expertise: Go Developer
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

More Questions

All questions

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind