Operation.cancel() had the same limitation, but it's easy to forget when Task makes cancellation look like a single, clean call. Calling task.cancel() doesn't stop anything by itself — it marks the task cancelled and leaves the running body to notice.
#The old way
final class ImageDownloadOperation: Operation {
let url: URL
init(url: URL) { self.url = url }
override func main() {
while !isCancelled {
performDownloadChunk(url: url)
}
}
func performDownloadChunk(url: URL) {}
}
#The new way
struct SearchResult { let query: String }
func search(_ query: String) async throws -> [SearchResult] {
var results: [SearchResult] = []
for page in 1...20 {
try Task.checkCancellation()
let batch = try await fetchPage(query: query, page: page)
results.append(contentsOf: batch)
}
return results
}
func fetchPage(query: String, page: Int) async throws -> [SearchResult] {
[SearchResult(query: query)]
}
final class SearchController {
private var currentTask: Task<Void, Never>?
func runSearch(for query: String) {
currentTask?.cancel()
currentTask = Task {
do {
let results = try await search(query)
print("Found \(results.count) results")
} catch is CancellationError {
print("Search cancelled")
} catch {
print("Search failed: \(error)")
}
}
}
}
#Why it matters
- Replacing an in-flight search task when the user types again avoids racing stale results against fresh ones
Task.checkCancellation()throwsCancellationError, which flows through existingtry/catchinstead of needing a separate cancelled-state check- Cancellation propagates automatically from a parent task to every child created inside a
TaskGrouporasync let Task.isCancelledis available for code paths that want to react without throwing, such as returning a partial result
#Gotcha
Cancellation does not interrupt currently executing code — a long synchronous loop or an await on a call that itself never checks cancellation will run to completion regardless of cancel() having been called. Every loop iteration or long-running step in async code needs an explicit try Task.checkCancellation() or Task.isCancelled check; without one, cancelling the task is a no-op until the next suspension point that happens to check.