Swift 5.8 ThrowingTaskGroup.waitForAll()

I stumbled across the planned changes for TaskGroup ThrowingTaskGroup.waitForAll() and I welcome the documentation update.

Thus far, I have only observed the changes in the Linux swift:nightly-5.8 images and not in any of the Xcode 14.3 betas (including running on iOS 16.4 / macOS 13.3).

I am wondering when/if other platforms will include this fix for Swift 5.8 (@backDeployed :pray:t2:) or if it will only be available on latest versions.

For those who are unfamiliar with the change it is subtle. Swift 5.7 and earlier never print "Gone Fishing :tropical_fish:" because waitForAll() immediately throws the first error it receives from the group body causing the remaining tasks to be cancelled.

try await withThrowingTaskGroup(of: Void.self) { group in
  group.addTask { throw CancellationError() }
  group.addTask {
    try await Task.sleep(nanoseconds: 1_000_000_000)
    print("Gone Fishing 🐠")
  try await group.waitForAll()

Swift 5.8 (so far only on Linux) will print "Gone Fishing :tropical_fish:" because waitForAll() actually waits for all tasks to complete before throwing the first error it received.


This fix is still not available within the Xcode 14.3 Release candidates creating an awkward mismatch in behaviour across platforms when using Swift 5.8.

The fix appears in:
Swift 5.8 Linux
Swift version 5.8-dev (LLVM f0fb631dd1a3a29, Swift ef7a6b85c8f7360)
Swift 5.9 Apple (dev toolchain)
Apple Swift version 5.9-dev (LLVM a34ab3cb279018d, Swift 21109a399e99cbb)

The fix does not appear in:
Swift 5.8 Xcode 14.3
Apple Swift version 5.8 (swiftlang- clang-1403.

cc @Ben_Cohen, @tomerd as release managers for 5.8 overall and Linux platform respectively

1 Like

Now that Swift 5.8 has shipped at least I can confirm that this discrepancy between Apple platform SDKs and the Linux toolchain provided SDK remained in the released toolchains.

The expected / documented / fixed behavior is on all platforms in 5.9, but sadly did not make it into Apple platform SDKs in the 5.8 release due to an unfortunate combination of more or less related circumstances.

Currently, the workaround is to use the implementation body of the correct version in your projects, or implement the "wait dance" manually.

   // @_alwaysEmitIntoClient
   public mutating func waitForAll() async throws {
     var firstError: Error? = nil
    // Make sure we loop until all child tasks have completed
    while !isEmpty {
      do {
        while let _ = try await next() {}
      } catch {
        // Upon error throws, capture the first one
        if firstError == nil {
          firstError = error
    if let firstError {
      throw firstError

The previous implementation was incorrect in the sense that it would not actually "await all", and just exit at the first error which was incorrect, however you are right to point out that it was a behavior change of this method which can be unexpected and problematic.

At least it is possible to write the expected behavior in pure Swift so a workaround is possible for those working on 5.8. Extensive use of waitAll is also somewhat unexpected, and most places where one would use it can be replaced with looping over the group. I've seen a lot of users waitAll before the end of the task group body, where it effectively does nothing since the group does such awaiting always anyway.

But yes, it is unfortunate to have this behavior divergence between platforms in 5.8. The change would have to land in an SDK containing Swift 5.8 to be effective, as it is not a compiler change.

Thankfully from 5.9 onwards platforms share the expected ("new") behavior.

Do note however that a task group always awaits all of its child tasks before returning from the with... { ... } function body; so if you find yourself sprinkling waitForAll at the end of task group usages, such method call is entirely un-necessary.