Go Context Cancel Propagation
The Point
An optional external call should not share a cancellable context – if that call errors and triggers cancel, the entire context is cancelled, affecting operations that shouldn’t be impacted.
Explanation
Scenario from a PR review:
1// ❌ Problematic version
2func (s *Service) GetCampaign(ctx context.Context) (*Campaign, error) {
3 ctx, cancel := context.WithCancel(ctx)
4 defer cancel()
5
6 // campaign components are optional
7 components, err := s.client.FetchComponents(ctx)
8 if err != nil {
9 return nil, err // defer cancel() fires here, ctx is cancelled
10 }
11
12 // all subsequent operations using the same ctx are affected
13 result, err := s.doSomethingElse(ctx)
14 ...
15}The problem: FetchComponents fails -> returns error -> defer cancel() executes -> this ctx is cancelled. If this ctx is shared elsewhere, everything blows up together.
Since campaign components are optional, the correct approach is to absorb the error from this call and not propagate it upward:
1// ✅ Correct version: optional call errors don't affect the main flow
2func (s *Service) GetCampaign(ctx context.Context) (*Campaign, error) {
3 // don't create a cancel context here
4
5 components, err := s.client.FetchComponents(ctx)
6 if err != nil {
7 // optional -- log and continue, don't return error
8 log.Warn("failed to fetch optional components", "err", err)
9 components = nil
10 }
11
12 result, err := s.doSomethingElse(ctx)
13 ...
14}Knowledge Sugar
Context cancellation propagation direction
Cancellation only propagates from parent to child, never in reverse. However, cancel() directly cancels the ctx you created – if other code also holds a reference to this ctx (or child contexts derived from it), they all get cancelled together.
If you need to fully isolate an optional call (unaffected by parent cancellation)
Go 1.21+ has context.WithoutCancel():
1// create a ctx that doesn't inherit the parent's cancel signal
2detachedCtx := context.WithoutCancel(ctx)
3components, err := s.client.FetchComponents(detachedCtx)This way, even if the parent request is cancelled (e.g. client disconnects), this optional call still runs to completion.
When to use which approach:
| Scenario | Approach |
|---|---|
| Optional call error shouldn’t affect main flow | Absorb the error, don’t return it |
| Optional call shouldn’t be affected by parent cancellation | context.WithoutCancel() |
| Optional call needs its own timeout | context.WithTimeout(context.WithoutCancel(ctx), ...) |