Go

What does this print? (Cause across WithoutCancel)

Question 291HardGo 1.22 to 1.25
var errShutdown = errors.New("shutting down")

func main() {
	parent, cancel := context.WithCancelCause(context.Background())
	detached := context.WithoutCancel(parent)
	child, cancelChild := context.WithCancel(detached)
	defer cancelChild()

	cancel(errShutdown)
	fmt.Println(parent.Err(), "|", context.Cause(parent))
	fmt.Println(detached.Err(), "|", context.Cause(detached))
	fmt.Println(child.Err(), "|", context.Cause(child))

	cancelChild()
	fmt.Println(child.Err(), "|", context.Cause(child))
}

Output:

context canceled | shutting down
<nil> | <nil>
<nil> | <nil>
context canceled | context canceled
  • detached has a nil Done() channel, so when child is created, propagateCancel sees a parent that can never be canceled and registers nothing. Canceling parent does not reach child.
  • context.Cause finds the nearest *cancelCtx through an internal Value lookup. WithoutCancel deliberately stops that lookup, so Cause(detached) falls back to detached.Err(), which is nil. It does not leak the parent's "shutting down" cause.
  • For child, the lookup finds child itself. Before cancellation its cause is nil. After a plain cancelChild(), the cause is context.Canceled.
  • Ordinary values (trace IDs, auth) still flow through detached; only cancellation, deadline, and cause are cut off.

More on Context

All 35 Context questions