Vitaly Batrakov
Hi Vitaly,
thank you for your fast and elaborate reply! I guess my initial post was a bit in a hurry, so I'll give it a second try and more time :)
"""I assume you are referencing to error propagation rule example with async lets"""
=> Yes exactly, that was were my comment was aimed at
"""If I add "try? await Task.sleep(nanoseconds: 8_000_000_000)" right after "try await (f, s)", it will change nothing because when TestError1 is propagated outside local scope, everything after "try await (f, s)" will be abandoned and not be executed. slow() will be cancelled for the same reason as it cancelled in original example. If I got your idea in wrong way, please let me know."""
=> I'm sorry, I wasn't very clear with thoughts. So first of all I was considering the """async let f = fast(); async let s = slow(); try await (f, s)""" not to be a function but being top-level code. I also changed "try" to "try?" so I wouldn't get a fatalError due to being top-level code. In that scenario that top level scope would not result in a fatalError / exception and therefore the slow() task wouldn't necessarily get cancelled - it only gets cancelled when the top level code ends.
The point I was trying to make is that it's not the fast() task throwing an error that is cancelling the slow() task, but leaving the scope, because the current level of scope is left, either (a) because an error or propagated and current level of scope its exited with an error or (b) due to exiting the current level of scope in a "normal" way.
I apologize that I misunderstood your idea in the first place. To help others in the same mind-space as myself, maybe you could extend your example with try await (f, s) being inside a function?
Thank you again for taking your time and deep dive into my comment! Really appreciate that.
Kind regards,
Heiko
Here are some output snippets, that might help to elaborate the point I was trying to make - but I guess it's based on the misinterpretation of top-level code vs. code inside a function:
=> Case 1: top level code + try (just added Timestamps)
Output:
2025-04-17 19:09:15 +0000 slow started
2025-04-17 19:09:15 +0000 fast started
2025-04-17 19:09:20 +0000 fast ended
2025-04-17 19:09:20 +0000 slow cancelled CancellationError()
2025-04-17 19:09:20 +0000 slow ended
Swift/ErrorType.swift:200: Fatal error: Error raised at top level: async_let_test.TestError1()
=> Case 2: top level code + try?
Output:
2025-04-17 19:10:24 +0000 fast started
2025-04-17 19:10:24 +0000 slow started
2025-04-17 19:10:30 +0000 fast ended
leaving local scope
2025-04-17 19:10:30 +0000 slow cancelled CancellationError()
2025-04-17 19:10:30 +0000 slow ended
Program ended with exit code: 0
=> Case 3: top level code + do { try await (f, s) } catch { print(Date.now, "caught", error) }
2025-04-17 19:12:24 +0000 fast started
2025-04-17 19:12:24 +0000 slow started
2025-04-17 19:12:29 +0000 fast ended
2025-04-17 19:12:29 +0000 caught TestError1()
leaving local scope // last statement. Top level code want to quit, but due to structured concurrency it cancels all child tasks and awaits for them
2025-04-17 19:12:29 +0000 slow cancelled CancellationError()
2025-04-17 19:12:29 +0000 slow ended
Program ended with exit code: 0
=> Case 4: top level code + do { try await (f, s) } catch { print(Date.now, "caught", error) } + try? await Task.sleep(nanoseconds: 6_000_000_000)
2025-04-17 19:17:51 +0000 slow started
2025-04-17 19:17:51 +0000 fast started
2025-04-17 19:17:56 +0000 fast ended
2025-04-17 19:17:56 +0000 caught TestError1()
2025-04-17 19:18:02 +0000 slow ended // due to the extra wait before leaving local scope (here: top level code): no cancellation necessary
leaving local scope
Program ended with exit code: 0
=> Case 5: using an asynchronous throwing function and call it with a do try catch block from top level code (and no additional Task.sleep() ):
func test() async throws {
async let f = fast()
async let s = slow()
try await (f, s)
print("leaving local scope")
}
do {
try await test()
} catch {
print(Date.now, "external catch", error)
}
2025-04-17 19:19:35 +0000 fast started
2025-04-17 19:19:35 +0000 slow started
2025-04-17 19:19:41 +0000 fast ended
2025-04-17 19:19:41 +0000 slow cancelled CancellationError() // local scope of try { } block wants to quit and continue with catch { } block
2025-04-17 19:19:41 +0000 slow ended
2025-04-17 19:19:41 +0000 external catch TestError1()
Program ended with exit code: 0
Fabio Sarmento
Artificial Intelligence
great breakdown of structured vs unstructured tasks, I've definitely run into some confusion around this in past projects. tbh, understanding the nuances really helps avoid those pesky race conditions. how do you usually handle task management in your own work?