Errors thrown in a child task are not handled until the child task is awaited, and when you write let c = try await a + b, the compiler is really doing something like this under the hood:
let a_temp = try await a
let b_temp = try await b
let c = a_temp + b_temp
This means that what you’re seeing is expected behavior: when you await on computeA first, your checkCancellation call doesn’t even see the fact that computeB threw an error until after computeA completes. When you reverse the ordering, the parent task sees the error earlier and marks itself (and thus its child tasks) as cancelled and computeA can then respond cooperatively to the cancellation (in your case, just print its cancellation status).
It is unintuitive to me though. I would expect let c = try await a + b to await child tasks in the order they finish rather than their position in the expression, similar to the vegetable chopping example that you linked.
These are asynchronous tasks so would run in parallel, based on the output computeB throws the error first, computeA continues execution and before returning Task.isCancelled is still false
In the example shown above computeA was never cancelled.
This is not what the video is implying, the more correct way of stating the behavior is:
when the parent task is cancelled, it cancels the remaining child tasks.
And the parent task is only cancelled when the error from the child task is actually propagated to the parent (via an await). Note that the specific example in the video has two explicit awaits and they only talk about what happens when the first throws an error - this corresponds to your try await b + a example.
Note that I do think that this behavior is a little unintuitive, but it is not a bug or unexpected, at least as I understand the structured concurrency architecture. Additionally, the exact ordering of which of a or b is awaited on first is likely an implementation detail that may change in the future (or with a different level of optimization), so you may want to be careful relying on this exact behavior.
When it throws an error or otherwise terminates itself.
No. The child task is marked as cancelled, but it is running in the "background" and the parent task is not notified of its status. The parent task only gets notified of child task status when it calls await, and it's at that point that any errors thrown by the child task are propagated (and this can then lead to the cancellation of the parent task, see above).
Yes, once the parent task is cancelled all outstanding child tasks are also marked as cancelled.
One key point about the above is the parent task can only await on a single child task at a time, meaning that if computeA is being awaited on while computeB throws an error in the background, the parent will not see any status from computeB until after computeA completes and then computeB is awaited on. If. you need this sort of "await on multiple tasks simultaneously, taking the first completed result" behavior, use an explicit TaskGroup instead, with the next() function returning the tasks in the order in which they completed.
Thanks a lot @MPLewis for the detailed explanation
sorry to drag on with this, but just had one more doubt:
It seems like moment when we use async let doesn't start the async child tasks, which sees odd and different from my understanding
func checkCancellation() async throws {
async let a = computeA()
async let b = computeB()
print("before loop")
for _ in 1...19000000 {
}
print("after loop")
let c = try await a + b
print("c = \(c)")
}
Output:
before loop
after loop
A - started
B - going to throw
A - going to return, Task.isCancelled = false
error: infinity
Are you running this in the Xcode simulator? There are limitations in the simulator as to how many threads are running at once which could explain that behavior. Try running on an actual device if you can.
More to the point, this is concurrency: there are basically no guarantees as to the order in which things execute. Even when running on an actual device, there’s a possibility you get the behavior you experienced where the parent gets all the way to its await before the children start - it all depends on the exact state of the device you’re running on at the moment, and will be affected by (for instance) other applications running at the same time.