Both async let and task groups are structured concurrency, notably enjoying automatic propagation of cancellation. See SE-0317, which says:
Later it says:
You go on to ask:
In contrast to async let, Task {…} is unstructured concurrency, meaning, amongst other things, you do not enjoy automatic cancellation propagation. And if you want to enjoy cancellation propagation with unstructured concurrency, you have to manually do this yourself with, for example, withTaskCancellationHandler.
Furthermore, if you create a Task {…} (unstructured concurrency) and do not explicitly await it, it will continue to run even after it falls out of scope. If you want to cancel it later, you have to save the reference to the Task somewhere and manually cancel it yourself.
With async let, however, you enjoy automatic cancellation propagation. Also, if you let it fall out of scope and neglect to await it, its task will be immediately cancelled (and, coincidentally, implicitly await that task to respond to the cancellation). See SE-0317 – Implicit async let awaiting.
Finally, you asked:
In practice, they’re generally talking about async let that you will eventually await.
There generally is little point in ever doing async let without an explicit await because if it falls out of scope before you await, it will be immediately cancelled (unlike Task {…}), as I discussed above. That having been said, we consciously leverage this at times, e.g., for early-exit scenarios, where this simplifies our code not having to manually cancel tasks that we no longer need.
But, unlike structured concurrency, when you use async let, you always await, generally with an explicit await, and occasionally relying on the implicit cancel/await logic.