[Pitch] Task Pools

I don‘t think it’s correct to say it “affects unhandled errors only.” Auto-cancellation affects errors that are not handled within the Task. I think it’s perfectly reasonable to want to handle errors centrally, outside the individual tasks, and for the client to decide whether the failure of an individual task should cause the failure of all other tasks:

/*
 * simple-rsync.swift
 */

enum Command {
  case copy(from: Path, to: Path)
  case rename(from: Path, to: Path)
  case delete(at: Path)
}

struct PermissionsError : Error {
  let path: Path
}

func processCommandList(_ commands: [Command]) async {
    await withThrowingTaskPool { pool in
      for command in commands {
        // determine the operation to perform outside the task so we don’t have to copy the Command into the task
        switch command {
          case let .copy(from: source, to: dest): pool.addTask { try _copy(from: source, to: dest) }
          case let .rename(from: source, to: dest): pool.addTask { try _rename(from: source, to: dest) }
          case let .delete(at: path): pool.addTask { try _delete(at: path) }
        }
      }
    } taskCompletionHandler: { task /* : Task<Void, PermissionsError> */ in
      if case let .failure(error) = task.result {
        // like rsync(1), log an error for this operation, but continue with the others
        print("Insufficient permissions to access \(error.path)")
      }
    }
  }
}

To my eye, the biggest flaw with the current proposal is that a developer can write a task that doesn’t handle its own errors, and the compiler says nothing. The consequences manifest only at runtime, in the form of cancelling other tasks. “My sever is resetting all of its connections, but my logs only show extremely infrequent errors” sounds like a very confusing SEV to debug!

Edit: Per @ktoso’s comment about Task reducers, I think I can phrase my rsync example in those terms:

func processCommandList(_ commands: [Command]) async {
    do {
      try await withThrowingTaskPool { pool in
        for command in commands {
          // determine the operation to perform outside the task so we don’t have to copy the Command into the task
          switch command {
            case let .copy(from: source, to: dest): pool.addTask { try _copy(from: source, to: dest) }
            case let .rename(from: source, to: dest): pool.addTask { try _rename(from: source, to: dest) }
            case let .delete(at: path): pool.addTask { try _delete(at: path) }
          }
        }
      } reducingResults: { result /* : Result<Void, any Error> */ in
          guard let PermissionsError.failure(error) = task.result else {
            // bubble unknown errors up to outer do/catch
            return result
          }
          // like rsync(1), log an error for failing operations, but continue with the others
          print("Insufficient permissions to access \(error.path)")
          return nil // don’t bubble this error up
      }
    }
  } catch {
    // something other than a PermissionsError happened
    fatalError("Unhandled error: \(String(describing: error))")
  }
}