Skip to main content

How to cancel a Task

About 4 minSwiftArticle(s)bloghackingwithswift.comcrashcourseswiftxcodeappstore

How to cancel a Task 관련

Swift Concurrency by Example

Back to Home

How to cancel a Task | Swift Concurrency by Example

How to cancel a Task

Updated for Xcode 15

Swift’s tasks use cooperative cancellation, which means that although we can tell a task to stop work, the task itself is free to completely ignore that instruction and carry on for as long as it wants. This is a feature rather than a bug: if cancelling a task made it stop work immediately, the task might leave your program in an inconsistent state.

There are seven things to know when working with task cancellation:

  1. You can explicitly cancel a task by calling its cancel() method.
  2. Any task can check Task.isCancelled to determine whether the task has been cancelled or not.
  3. You can call the Task.checkCancellation() method, which will throw a CancellationError if the task has been cancelled or do nothing otherwise.
  4. Some parts of Foundation automatically check for task cancellation and will throw their own cancellation error even without your input.
  5. If you’re using Task.sleep() to wait for some amount of time to pass, cancelling your task will automatically terminate the sleep and throw a CancellationError.
  6. If the task is part of a group and any part of the group throws an error, the other tasks will be cancelled and awaited.
  7. If you have started a task using SwiftUI’s task() modifier, that task will automatically be canceled when the view disappears.

We can explore a few of these in code. First, here’s a function that uses a task to fetch some data from a URL, decodes it into an array, then returns the average:

func getAverageTemperature() async {
    let fetchTask = Task { () -> Double in
        let url = URL(string: "https://hws.dev/readings.json")!
        let (data, _) = try await URLSession.shared.data(from: url)
        let readings = try JSONDecoder().decode([Double].self, from: data)
        let sum = readings.reduce(0, +)
        return sum / Double(readings.count)
    }

    do {
        let result = try await fetchTask.value
        print("Average temperature: \(result)")
    } catch {
        print("Failed to get data.")
    }
}

await getAverageTemperature()

Download this as an Xcode projectopen in new window

Now, there is no explicit cancellation in there, but there is implicit cancellation because the URLSession.shared.data(from:) call will check to see whether its task is still active before continuing. If the task has been cancelled, data(from:) will automatically throw a URLError and the rest of the task won’t execute.

However, that implicit check happens before the network call, so it’s unlikely to be an actual cancellation point in practice. As most of our users are likely to be using mobile network connections, the network call is likely to take most of the time of this task, particularly if the user has a poor connection.

So, we could upgrade our task to explicitly check for cancellation after the network request, using Task.checkCancellation(). This is a static function call because it will always apply to whatever task it’s called inside, and it needs to be called using try so that it can throw a CancellationError if the task has been cancelled.

Here’s the new function:

func getAverageTemperature() async {
    let fetchTask = Task { () -> Double in
        let url = URL(string: "https://hws.dev/readings.json")!
        let (data, _) = try await URLSession.shared.data(from: url)
        try Task.checkCancellation()
        let readings = try JSONDecoder().decode([Double].self, from: data)
        let sum = readings.reduce(0, +)
        return sum / Double(readings.count)
    }

    do {
        let result = try await fetchTask.value
        print("Average temperature: \(result)")
    } catch {
        print("Failed to get data.")
    }
}

await getAverageTemperature()

Download this as an Xcode projectopen in new window

As you can see, it just takes one call to Task.checkCancellation() to make sure our task isn’t wasting time calculating data that’s no longer needed.

If you want to handle cancellation yourself – if you need to clean up some resources or perform some other calculations, for example – then instead of calling Task.checkCancellation() you should check the value of Task.isCancelled instead. This is a simple Boolean that returns the current cancellation state, which you can then act on however you want.

To demonstrate this, we could rewrite our function a third time so that cancelling the task or failing to fetch data returns an average temperature of 0. This time we’re going to cancel the task ourselves as soon as it’s created, but because we’re always returning a default value we no longer need to handle errors when reading the task’s result:

func getAverageTemperature() async {
    let fetchTask = Task { () -> Double in
        let url = URL(string: "https://hws.dev/readings.json")!

        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            if Task.isCancelled { return 0 }

            let readings = try JSONDecoder().decode([Double].self, from: data)
            let sum = readings.reduce(0, +)
            return sum / Double(readings.count)
        } catch {
            return 0
        }
    }

    fetchTask.cancel()

    let result = await fetchTask.value
    print("Average temperature: \(result)")
}

await getAverageTemperature()

Download this as an Xcode projectopen in new window

Now we have one implicit cancellation point with the data(from:) call, and an explicit one with the check on Task.isCancelled. If either one is triggered, the task will return 0 rather than throw an error.

Tips

You can use both Task.checkCancellation() and Task.isCancelled from both synchronous and asynchronous functions. Remember, async functions can call synchronous functions freely, so checking for cancellation can be just as important to avoid doing unnecessary work.

Similar solutions…
How to cancel a task group | Swift Concurrency by Example

How to cancel a task group
What’s the difference between a task and a detached task? | Swift Concurrency by Example

What’s the difference between a task and a detached task?
How to create and use task local values | Swift Concurrency by Example

How to create and use task local values
What’s the difference between async let, tasks, and task groups? | Swift Concurrency by Example

What’s the difference between async let, tasks, and task groups?
How to run tasks using SwiftUI’s task() modifier | Swift Concurrency by Example

How to run tasks using SwiftUI’s task() modifier

이찬희 (MarkiiimarK)
Never Stop Learning.