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.