AsyncSequence | Apple Developer Documentation in The docs it does not say what the user's responsibility is for cancellation of the task. From reading other posts it seems like AsyncSequences are supposed to terminate the sequence so the user does not need to handle it themselves, but, unless I'm missing something, the doc feels like it's missing discussing this part especially as Task cancellation is cooperative.
(hypothetically, some implementation of the AsyncSequence can just ignore the cancelled status and just emit until there's nothing left..)
How an AsyncSequence responds to cancellation is left up to each iterator implementation (as opposed to its end-of-iteration behavior). However, the AsyncIteratorProtocol documentation does somewhat imply that an iterator should respond appropriately to cancellation:
Types conforming to AsyncIteratorProtocol should use the cancellation primitives provided by Swift’s Task API. The iterator can choose how to handle and respond to cancellation, including:
Checking the isCancelled value of the current Task inside next() and returning nil to terminate the sequence.
Calling checkCancellation() on the Task, which throws a CancellationError.
Implementing next() with a withTaskCancellationHandler(handler:operation:) invocation to immediately react to cancellation.
If the iterator needs to clean up on cancellation, it can do so after checking for cancellation as described above, or in deinit if it’s a reference type.
On a related note, AI tools are very keen on checking cancellation in for await loops.
for await value in values.async {
guard !Task.isCancelled else { break }
// Actual work.
}
This isn't ever actually necessary, is it? In all of my testing, cancellation propagates to AsyncSequence in such a way that the loop is never called when cancelled.
It can produce different behavior, but likely not behavior you want.
For example, Async[Throwing]Stream always drain their buffer before responding to cancellation (otherwise the buffered items would be unrecoverably lost):
let (stream, continuation) = AsyncStream.makeStream(of: Int.self)
continuation.yield(1)
continuation.yield(2)
continuation.yield(3)
let task = Task {
withUnsafeCurrentTask { $0!.cancel() }
for await item in stream {
print(item)
}
}
await task.value
If the async sequence already checks for cancellation, you don't need to check it again inside the loop where you consume it.
When you are outside the loop, it might be a good idea to check if the sequence finished normally, or if it was cut short because of a cancellation request. Iterating through the sequence doesn't reveal the reason the sequence stopped, unless it's a throwing sequence and it propagates the CancellationError.
var count = 0
for await value in values.async {
count += 1
}
if !Task.isCancelled {
// `count` equals the length of `values`.
} else {
// `count` may be less than or equal the length of `values`.
}
if you need to check cancellation in the loop then I would claim the AsyncSequence adopting type has a bug - all SDK vended (standard library or ones in the macOS/iOS/* SDKs) ought to behave in one of two ways. Either a) it throws an error on cancellation or b) it returns nil. And nearly all of them do that as an early resume. So the AI generated code adding a check in there is completely not needed and likely just bad practice. Is it perhaps sometimes defensive? sure.... Is it worth it to ship that type of thing in production? no... I would claim that any time you need to do that someone should try and correct that.
The check for isCancelled or checkCancellation is not 100% free. It does have some accrued cost (albeit very small) so doing it 2x effectively means that you are technically wasting cycles on work that won't ever really impact anything.
The reason the documentation is a bit wishy-washy about it is that there is a potential of operators or custom implementations deliberately swallowing or ignoring cancellation for specialized reasons. So the protocol itself doesn't create that cancellation behavior, it is just a formality of a design-by-convention that it responds in some way to cancellation reasonably. The issue is that conveying "reasonable" is a very difficult task in so many fields; not to mention technical documentation.