Long-running operations are common in modern Swift applications—from network requests and file processing to background data synchronization.
But just as important as starting work is knowing when to stop it. Whether a user taps a cancel button, navigates away from a screen, or a higher-priority task takes over, your app needs a safe and predictable way to end work in progress. Swift’s structured concurrency model includes a thoughtful approach to handling exactly this situation: task cancellation.
Some possible scenarios for task cancellation are shown in this table.
| App | Action | Cancelable Task |
| Video editor | User closes their video project | Background render tasks |
| Image editor | User closes an image | Autosave task |
| Game | Main character dies | Animation and sound tasks |
| Cloud file browser | User deletes a cloud file | Sync task for that file |
| Music player | User skips to the next song | Streaming task for that song |
For such cases, Swift offers a built-in mechanism to safely cancel tasks. Unlike killing a thread, Swift tasks aren’t forcefully stopped. Instead, tasks are politely asked to cancel themselves. When a task notices that it needs to cancel, it has the chance to conduct any necessary cleanup steps and stop gracefully. This is called cooperative cancellation: Tasks must agree to stop and may decide how/when to stop.
Therefore, a task might never listen to cancellation requests or might hear them only to ignore them. When coding the task, the programmer is responsible for adding the cancellation logic if necessary; otherwise, the task will be uncancelable.
Now, let’s cancel some tasks! The listing below features an imaginary virus scan task, which is canceled after running for a while.
let virusScan = Task { print("Virus scan started")
while true {
if Task.isCancelled { print("Cancelling scan...") print("- Cleaning up temporary files") print("- Scan was cancelled") break }
print("Scanning next file") try? await Task.sleep(nanoseconds: 500_000_000) }}
Task { try? await Task.sleep(nanoseconds: 1_500_000_000) virusScan.cancel()}
First, let’s focus on the virusScan task. As the task proceeds through files, it checks for a cancellation request with if Task.isCancelled before scanning each file. That property returns true only if a cancellation was requested. By checking that property occasionally, the task gains the ability to stop in the middle of the process.
If the task’s code didn’t contain an if Task.isCancelled { … } block, then the task would never cancel—even if a cancellation was requested. The task is responsible for listening for requests, and it has the freedom to decide whether to honor requests or ignore them. But in this case, the task complies with the cancellation request, cleaning up temporary files and leaving the scan loop.
The client task merely waits for a short while and then asks the virusScan task to cancel, simulating a user canceling the scan via the UI.
The output of this example is shown in the figure below, which looks as expected. After virusScan task starts and scans a couple of files, it notices the cancellation request, taking the necessary cleanup steps and exiting the scan loop.
Checking Task.isCancelled is one option for task cancellation. An alternative is to call Task.checkCancellation(), which will deliberately throw an error if the task is canceled. That alternative might be a good option if you already have an error-handling mechanism, in which case task cancellation would be just another reason to end the task.
This listing demonstrates the usage of Task.checkCancellation(). In this example, if any error occurs during the virus scan, including a task cancellation, the task will cleanup and exit.
let virusScan = Task {
print("Virus scan started")
while true {
do { try Task.checkCancellation() // throws if cancelled print("Scanning next file") try? await Task.sleep(nanoseconds: 500_000_000) } catch { print("Cancelling scan...")
print("- Cleaning up temporary files")
print("- Scan was cancelled") break
} }}
Task { try? await Task.sleep(nanoseconds: 1_500_000_000) virusScan.cancel()}
Swift’s cancellation model reinforces an important principle of modern concurrency: tasks are cooperative, not disposable. Instead of being abruptly terminated, they’re given the opportunity to observe a cancellation request and respond appropriately. Whether you choose to poll Task.isCancelled or rely on Task.checkCancellation() and structured error handling, the key responsibility lies with the developer—your task must actively participate in its own cancellation.
By building cancellation checks into long-running operations, you ensure that your apps remain responsive, predictable, and safe. Proper cancellation handling isn’t just about stopping work; it’s about stopping it cleanly.
Editor’s note: This post has been adapted from a section of the book Swift: The Practical Guide by Kerem Koseoglu. Dr. Koseoglu is a seasoned software engineer, author, and educator with extensive experience in global software development projects. In addition to his expertise in ABAP, he is also proficient in database-driven development using Python and Swift. He is the author of Design Patterns in ABAP Objects (SAP PRESS), as well as several best-selling technical books in Turkey. He has a Ph.D. in organizational behavior.
This post was originally published 3/2026.