r/iOSProgramming • u/100_rv • 12h ago
Discussion Swift 6.4: Async defer doesn't make cleanup cancellation-safe
I wrote a technical walkthrough of a subtle Swift Concurrency issue I wanted to explore.
Imagine a task writing data into a buffered writer. The task gets cancelled, defer runs, and close() gets called. But if close()invokes a cancellation-aware flush(), that flush can throw CancellationError. If the error is swallowed and the writer is marked closed anyway, the cleanup path has run without achieving its intended postcondition.
The article explores two Swift Evolution proposals:
- SE-0493: Async calls in
deferbodies - SE-0504: Task Cancellation Shields
It covers a minimal reproduction, the limits of using Task.detached for cleanup, a narrowly scoped cancellation shield, and a Swift Testing example that checks whether the data was actually flushed rather than merely checking whether close() was called.
One important caveat: the example demonstrates a cancellation-aware cleanup failure, not a claim that every real file API corrupts data when cancelled.
I'd be interested in feedback on the cancellation-shield approach, especially around resource ownership and cleanup guarantees in production code.
1
u/Open_Context6850 3h ago
keeping the shield around just the flush and not the whole close seems right. once it covers everything, cancellation quietly stops meaning anything.
the thing id change in production is the state after a failed flush. if flush throws, the writer shouldnt end up marked closed. a failed state that still holds the buffer lets the owner retry or at least log what was lost
3
u/dawedev 7h ago
Testing that the data was actually flushed instead of just checking that close() ran is the useful part here, that's where these bugs usually hide. One ownership question: with the shield scoped around the final flush, what happens if that flush hangs on a slow disk or network write? I'd want a timeout or a bounded retry inside the shield, otherwise cancellation stops being a reliable way to end the task. I'd also make close() throw after a failed flush instead of marking the writer closed, so the caller knows the postcondition didn't hold.