Cancellation asks a child task to stop:
task := spawn { watch_changes() }
if should_stop() {
task.cancel()
}cancel() returns void. It is idempotent and is a no-op for an already
completed task.
Cancellation is a request, not a join. The call does not wait for the child to
finish cleanup and does not produce the child’s T. Keep task lifetime and
application result handling as separate decisions.
Cooperative delivery
Cancellation is observed by the scheduler and generated safepoints. It does not interrupt the child at an arbitrary machine instruction. A long-running operation must reach a scheduler, host, or loop safepoint before cancellation takes effect.
This preserves compiler-managed cleanup. Captured managed values and child frame locals are dropped along cancellation paths rather than abandoning raw storage.
User-defined Drop implementations for live owned locals also run through the
typed cancellation dispatcher. Cleanup is synchronous; a destructor cannot
perform another suspending operation.
Descendants
Tasks form a parent-child tree. Cancellation walks through descendant work, so canceling a task also prevents its subtree from continuing independently. This is structural cancellation, even when the parent discarded a source-level handle.
An explicit child cancellation and an ancestor cancellation converge on the same subtree model. Code should not depend on observing which ancestor first requested the stop.
Race losers
race performs cancellation automatically: after one arm wins, the other
spawned arm tasks receive cancellation requests.
select does not do this. It consumes the winning source and leaves losing
tasks under caller control. Track which arm won, then await or cancel only the
still-owned task; the winning handle has already been consumed and must not be
used again.
Shutdown protocols
For a producer using a stream, close the stream or cancel the producer according to who owns shutdown:
producer := spawn {
for item in source {
if !events.send_await(item) {
return
}
}
events.close()
}
if consumer_stopped() {
events.close()
producer.cancel()
}Closing wakes a producer parked on backpressure; send_await returns false.
Cancellation remains useful when the child can also be parked on another host
operation.
Cleanup
defer scopes still run on cancellation-driven unwinding where the generated
task cleanup path crosses them. Keep cleanup idempotent: normal completion,
early error, explicit cancellation, and parent shutdown may converge on the
same resource boundary.
Do not put essential externally visible commit logic only in a cancellation cleanup block. Cancellation cleanup should release or roll back owned state; successful publication belongs on the normal completion path.
Effects
An explicit cancellation operation contributes Cancel to the function’s
effect row. race contributes Spawn, Suspend, and Cancel.
Cancellation is not currently a typed Result returned from
Task[T].await(). Do not invent a Cancelled application error unless your
API intentionally maps task lifetime into its domain error contract.
select currently receives a conservative Cancel effect from semantic
lowering even though it leaves losing sources running. That static
over-approximation is not a promise of runtime loser cancellation.