I thought the Task cancellation would get passed down, and then bubble back up as the thrown error (without needing to explicitly check for cancellation). What am I missing here?
In the case you have, you'd typically just call the function directly: return try await get(url: url). But yes, if you want to propagate cancellation to an unstructured task, you need to catch it in withTaskCancellationHandler first:
This package from PointFree defines an extension on Task that, when awaited on, will propagate cancellation automatically (by using withTaskCancellationHandler). If you don't want to use the whole package, it's a pretty small extension you could copy into your project.
extension Task {
public var cancellableValue: Success {
get async throws {
try await withTaskCancellationHandler {
try await self.value
} onCancel: {
self.cancel()
}
}
}
}
FWIW, I'd carefully advise against using this pervasively, since typically the point of creating an unstructured Task is to create some sort of a "future" that multiple places would want to await (as again, for singular points of use, the standard try await or async let is sufficient).
If you enable cancelling a task this easily, you can have very weird effects where one piece of code that doesn't really need this value would happily cancel the task, but other places that depend on it more heavily wouldn't ever cancel it, yet the task still gets cancelled by that low-priority caller. It is as if an instance of a class was to be destroyed as soon as the first of potentially hundreds of references to it gets released — this is very asymmetric and can be very hard to debug.