Skip to content

Cancellation

Stop child work cooperatively and release task-owned resources.

Updated View as Markdown

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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close