With context.WithCancelCause, what do ctx.Err() and context.Cause(ctx) each return after cancellation?
answer
- two questions: what class, and why
- the cancel func grew a parameter
- the existing sentinel had to keep working
- read it back with a package-level function
- only the first cancellation records a reason
basics
~10 sctx.Err() still returns context.Canceled, the generic class of ending. context.Cause(ctx) returns the specific error handed to the cancel func. Before any cancellation, Cause returns nil; if cancel was called with nil, Cause matches ctx.Err().
solid answer
~50 s`context.WithCancelCause` returns the derived context and a `context.CancelCauseFunc`, which takes an error: `cancel(err)`. Cancelling behaves exactly as `WithCancel` does — `Done()` closes and `ctx.Err()` reports `context.Canceled` — so nothing about the existing contract changes and callers comparing against `context.Canceled` keep working. The reason lives in a second channel: `context.Cause(ctx)` returns the error you passed. If cancel was called with `nil`, `Cause` falls back to the same value as `ctx.Err()`; if the context has not been cancelled at all, `Cause` returns `nil`. Only the *first* cancellation in the chain sets the cause, so a later `cancel(other)` does not overwrite it, and a child asked for its cause reports the ancestor's. It exists because `context.Canceled` alone tells you that something stopped, not what stopped it — which is the difference between a usable log line and a shrug.
code
go · 10 linesvar errSourceGone = errors.New("source stream disappeared")
ctx, cancel := context.WithCancelCause(parent)
defer cancel(nil) // ordinary cleanup, no reason attached
cancel(errSourceGone)
<-ctx.Done()
_ = ctx.Err() // context.Canceled
_ = errors.Is(context.Cause(ctx), errSourceGone) // truego deeper
Know only that a variant exists which lets whoever cancels attach an error explaining why, and that the plain cancellation sentinel still shows up in ctx.Err().
Be able to state both return values precisely: Err gives context.Canceled, Cause gives the attached error, nil before cancellation, and the Err value when cancel was called with nil.
Show when it earns its place: several distinct reasons can end the same work and your logs or your caller must tell them apart. Mention that only the first cancellation records a cause and that causes propagate to descendants.
Treat it as a diagnosability decision. Cancellation reasons defined as sentinel errors turn every abandoned unit of work into a countable category, which is what lets you distinguish a deliberate shutdown from a dependency quietly failing.
## The problem it solves Plain `context.WithCancel` gives you exactly one bit of information at the receiving end: the context is done, and `ctx.Err()` says `context.Canceled`. In a system where several things can cancel the same work — the caller went away, a sibling attempt already won, a supervisor is shutting the runner down, an input turned out to be corrupt — that single sentinel is the same in all four cases. The goroutine that returns it cannot log a useful reason, and the layer above cannot decide whether to report a failure or a normal shutdown. `context.WithCancelCause` closes that gap by letting the *canceller* attach an error explaining itself. ## The API ``` func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) type CancelCauseFunc func(cause error) func Cause(c Context) error ``` The derived context is an ordinary `Context` — there is no new interface and no new method. The difference is entirely in the cancel func, which now takes an error, and in the package-level `Cause` function that reads it back. ## What each accessor returns After `cancel(errors.New("source stream went away"))`: - `ctx.Done()` is closed, as with any cancellation. - `ctx.Err()` returns `context.Canceled`. **It does not return your error.** This is deliberate: `Err()` reports the *class* of ending, and every piece of existing code that compares against `context.Canceled` must keep working. Changing `Err()` would have been a breaking change to the whole ecosystem. - `context.Cause(ctx)` returns the error you passed. The other cases: - **Not cancelled yet** — `Cause` returns `nil`, the same way `Err()` does. - **Cancelled with `cancel(nil)`** — `Cause` returns the same value as `Err()`, so you get `context.Canceled` rather than a surprising nil on a done context. - **A plain `WithCancel` context** — `Cause` still works on it and returns whatever `Err()` returns. `Cause` is safe to call on any context, so a generic helper can use it unconditionally. ## First cancellation wins The cause is set once, by the first cancellation that reaches the context, and never overwritten. Two consequences: - Calling `cancel(a)` and then `cancel(b)` on the same context leaves the cause as `a`. The second call is a no-op, exactly as a repeated plain cancel is. - Causes propagate downward. If an ancestor is cancelled with a cause, every descendant's `Cause` reports that ancestor's error, because the ancestor's cancellation is the first one that reached them. A child cancelled by its own cancel func first keeps its own cause. That propagation is what makes it useful across layers: a worker deep in a call chain can ask `context.Cause(ctx)` and learn the reason recorded several frames up, without anyone threading a reason parameter through. ## How you actually use it The pattern is to define sentinel errors for the reasons that matter, cancel with them, and inspect at the point where the decision is made: ``` var errSourceGone = errors.New("source stream disappeared") ctx, cancel := context.WithCancelCause(parent) defer cancel(nil) // still mandatory; nil means no specific reason ... cancel(errSourceGone) ``` and downstream: ``` if err := ctx.Err(); err != nil { return fmt.Errorf("transcode stopped: %w", context.Cause(ctx)) } ``` Note that `defer cancel(nil)` is still required. `WithCancelCause` derives a child exactly as `WithCancel` does, with the same registration in the parent's children set, so an uncalled cancel retains the child in exactly the same way. Passing `nil` is the idiomatic way to say 'this is the ordinary cleanup call, no reason attached'. ## Where it fits and where it does not Use it when several distinct reasons can end the same work and downstream code or your logs need to tell them apart. Do not use it as a general error-return channel: the cause is attached to a *cancellation*, so it is visible only once the context is done, and code that never checks `Done()` will never see it. The function's return value is still how errors travel. Also resist the urge to attach large objects as causes. The cause is retained on the context for as long as the context is, which for a long-lived cancelled parent is the rest of the process. A sentinel error, or a small wrapped error, is the right size. ## Interop Because the cause is an ordinary error, everything in `errors` works on it: `errors.Is(context.Cause(ctx), errSourceGone)` is the normal test, and a wrapped cause unwraps as usual. That is the whole point of using an error value rather than a string.
- Why does ctx.Err() not simply return the cause?Compatibility and layering. Every existing caller compares `ctx.Err()` against `context.Canceled` to recognise an orderly stop; returning an arbitrary error there would break all of them. `Err()` reports the class of ending, `Cause` the specific reason, and code picks the one it needs.
- What does context.Cause return if you call the cancel func twice with different errors?The first error. The cause is recorded once, by the first cancellation to reach the context, and later calls are no-ops just as repeated plain cancels are. The same rule makes a descendant report an ancestor's cause when the ancestor was cancelled first.
- Is defer cancel(...) still required with WithCancelCause?Yes. It derives a child and registers it with the parent exactly as `context.WithCancel` does, so an uncalled cancel retains the child for as long as the parent lives. `defer cancel(nil)` is the idiomatic form: cleanup with no particular reason attached.
Err() is the cause of death on a certificate's summary line — 'cancelled'. Cause is the narrative underneath it. The summary line stays standard so existing paperwork still parses.
saying these in an interview costs you the question
- Expects ctx.Err() to return the error passed to cancel
- Thinks a second cancel call overwrites the recorded cause
- Uses the cause as the normal way to return errors from a function
- Assumes context.Cause panics on a context made by WithCancel
- Skips defer cancel because the cause variant looks different