Context Cancellation in Go: Three Patterns I Use Every Day
1 min read
The problem
When we first adopted Go at work, context.Context felt like boilerplate. Pass it everywhere, check .Done() in loops, done. Then a cascading timeout brought down three services because one context was silently swallowed.
Pattern 1: Always chain with WithTimeout, never WithCancel alone
// Bad — no deadline means the context lives forever
ctx, cancel := context.WithCancel(parentCtx)
// Good — timeout bounds the lifetime
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()
If you don’t set a timeout, you’re just adding ceremony.
Pattern 2: Return the context’s error, not a wrapper
When a function takes a context, its errors should expose whether cancellation caused the failure:
if ctx.Err() != nil {
return 0, fmt.Errorf("fetch: %w", ctx.Err())
}
Callers can then use errors.Is(err, context.DeadlineExceeded) to decide retry vs abort.
Pattern 3: Don’t store contexts in structs
The standard library’s own http.Request carries a context. That doesn’t mean your struct should. Pass them explicitly to methods that need them.
What I learned
Treat context like a request-scoped value. The moment you stash it in a field, you’ve lost the boundary.