Go’s context.Context propagates deadlines and cancellation across API boundaries. Traditional cancellation tells downstream work that it should stop, but ctx.Err() only reports context.Canceled or context.DeadlineExceeded. Sometimes the reason matters.
Go 1.20 introduced context.WithCancelCause, and later releases added cause-aware deadline helpers. They preserve a domain error without changing normal cancellation behavior.
Attach a cause to cancellation
package main
import (
"context"
"errors"
"fmt"
)
var ErrSuperseded = errors.New("request superseded")
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(ErrSuperseded)
fmt.Println(ctx.Err()) // context canceled
fmt.Println(context.Cause(ctx)) // request superseded
}Code that only understands Context still sees ordinary cancellation. Code that needs diagnostic detail can call context.Cause.
Use causes for coordination, not control flow everywhere
A cause is useful when one goroutine stops siblings for a meaningful reason: an upstream stream closed, a newer request replaced an older one, or a required dependency failed.
ctx, cancel := context.WithCancelCause(parent)
defer cancel(nil)
go func() {
if err := consume(ctx); err != nil {
cancel(fmt.Errorf("consumer failed: %w", err))
}
}()Workers should still select on ctx.Done() and return promptly. The cause explains cancellation; it does not replace the cancellation channel.
Preserve parent cancellation semantics
Cancellation propagates from parent to child. The first applicable cancellation cause wins according to the context tree. Do not assume a child can overwrite an already-canceled parent with a more convenient error.
When a deadline is the meaningful reason, context.WithDeadlineCause or context.WithTimeoutCause can attach a stable cause while preserving deadline behavior.
Keep causes safe to log
Cancellation causes often reach logs and traces. Do not embed credentials, tokens, raw request bodies, or other sensitive values in errors. Prefer typed or sentinel errors plus safe metadata recorded separately.
Common pitfalls
Checking only the cause
Use ctx.Err() when code merely needs to know whether work was canceled. Reach for context.Cause when the reason changes diagnostics or error reporting.
Forgetting cleanup
Cause-aware contexts still need their cancel function called when work completes early. defer cancel(nil) is a useful default after successful construction.
Treating cancellation as failure
A superseded search request or disconnected client may be expected behavior. Preserve the cause, but classify it appropriately in metrics so normal cancellation does not inflate error rates.
Passing contexts into structs
The standard Go convention still applies: pass context.Context explicitly as the first parameter to operations rather than storing it for arbitrary future work.
A useful boundary
Cancellation answers “should this work stop?” and a cause can answer “why did it stop?” Keeping those responsibilities separate lets existing context-aware code remain simple while giving orchestration layers enough information to produce better logs, traces, and user-facing errors.