The problem is not cancellation, per se, but timing: f1 passed the checkCancellation line (and possibly even finished) before you cancelled t1. f1 is an async function, but isn’t really doing anything asynchronous within it, so it finishes execution exceptionally quickly. There is a race between t1 being canceled and f1 checking for cancellation.
However, if your function does something that really is asynchronous (and does something that responds to cancellation), then you see the behavior you are looking for. For example, below, f2 takes 10 seconds, but t2 is cancelled after one second, so we can see cancellation in action:
nonisolated func f2() async throws {
try await Task.sleep(for: .seconds(10))
print("inside f2")
}
let t2 = Task {
try await f2()
}
Task {
try await Task.sleep(for: .seconds(1))
t2.cancel()
print("cancelled before `f2` got to `print` statement")
}
FWIW, the withTaskCancellationHandler solves a very different problem: It‘s for propagating cancellation when using unstructured concurrency:
func foo(url: URL) async throws {
let task = Task {…}
return try await withTaskCancellationHandler {
try await task.value
} onCancel: {
task.cancel()
}
}
This way, if you cancel foo, it will cancel the unstructured concurrency within foo. If you don’t use withTaskCancellationHandler, the cancellation of foo would not cancel the unstructured concurrency within.
(As an aside, this is one of the reasons why we generally prefer structured concurrency, because cancellation is propagated for us automatically. But where we really need unstructured concurrency, we always use this withTaskCancellationHandler pattern, to remedy this lack of automatic cancellation propagation.)
As to your broader question, “How to cancel an async func”, there are a ton of different cancellation patterns:
-
The cancelling of the unstructured task with asynchronous work, as outlined at my f2 example, above, is a fine way to do it (with the typical caveats about unstructured concurrency, notably that you have to handle cancellation propagation manually, that the called function supports cancellation, etc.).
-
If you are using SwiftUI, it does some automatic cancellation for you. For example, this will start foo when the view is presented, but the .task {…} view modifier will cancel it for you when the view is dismissed:
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {…}
.task {
await foo()
}
}
func foo() async {…}
}
There is no need to manually cancel foo in this case.
-
If you use task groups, you can cancel the group when one of them fails. Or if you use a discarding task group, it will automatically do this for you:
func foo() async throws {
try await withThrowingDiscardingTaskGroup { group in
group.addTask { try await f3() }
group.addTask { try await f4() }
}
}
In this case, if either f3 or f4 throws an error, the other will be cancelled (if it hasn’t finished already).
-
If you are using async let, if you let the task fall out of scope without awaiting it, it will be cancelled. For example, this example is adapted from the one in SE-0317:
func go() async {
async let f = fast() // 300ms
async let s = slow() // 3seconds
return "nevermind..."
// implicitly: cancels f
// implicitly: cancels s
// implicitly: await f (giving it a chance to respond to the cancellation)
// implicitly: await s (giving it a chance to respond to the cancellation)
}
So, if you really are just trying to cancel some unstructured concurrency, then option 1 is sufficient. But for the broader question of how to cancel an async function, as you can see, there are a plethora of alternatives. It boils down to the particular problem that you are trying to solve.
Implicit in all of these examples is that the function you are calling must respond to cancellation. Fortunately, most of the async functions we call (e.g., Task.sleep, URLSession, etc.) already do this for us. As a result, we don’t tend to use Task.checkCancellation (which checks for cancellation, and if not cancelled, just proceeds to the next line) very often: Its use is largely limited to our own custom compute tasks where we periodically would check for cancellation inside a loop, or what have you. But that’s a bit of an edge case.