Sticky Error
Sticky error pattern is an idiomatic way of getting the first error from a list of goroutines that are treated as one
functional block. It’s especially useful when using concurrency primitives like sync.WaitGroup 1. When we query
multiple sources for some data using goroutines and want to get only the first encountered error and do not care about
other errors. Any error in the block means the entire operation failed and is in an invalid state. The sticky error
pattern is perfect for this scenario:
type DashboardFetcher struct {
once sync.Once
wg sync.WaitGroup
stickyErr error
// other fields omitted for simplicity
}
func (d *DashboardFetcher) stickError(err error) {
// this ensures only the first error will be recorded
d.once.Do(func() {
d.stickyErr = err
})
}
func (f *DashboardFetcher) fetchOrder(ctx context.Context) {
defer f.wg.Done()
var order Order
if err := getJSON(ctx, "https://example.com", &order); err != nil {
f.stickError(fmt.Errorf("failed to get order: %w", err))
return
}
// set data here...
}
func (d *DashboardFetcher) Fetch(ctx context.Context) (DashboardData, error) {
d.wg.Add(3)
go d.fetchUser(ctx)
go d.fetchProduct(ctx)
go d.fetchOrder(ctx)
d.wg.Wait()
return d.data, d.err
}
All goroutines must run to completion. We get an error (or the data) only after all goroutines finish execution. That’s why it’s important to pass a context to them with some timeout set (do not create goroutines if You don’t know when their exeuction ends).
In the real-world scenario there is already a package that You can use errgroup 2.
The above sync.Once version is worth knowing because it shows you how errgroup works under the hood and because
sometimes you need more control than the errgroup gives us (like collecting all errors, not just the first).