Typed throws do not infer type in generic type-throwing functions

Typed throws in generic function are not typed throws

@Sendable
public func withSingleTaskInThrowingTaskGroup<Returning: Sendable, ThrowsError: Error>(
    cancellationError: ThrowsError,
    perform: @escaping @Sendable () async throws(ThrowsError) -> Returning
) async throws(ThrowsError) -> Returning {
    do {
        return try await withThrowingTaskGroup(of: Returning.self, returning: Returning.self) { taskGroup in
            taskGroup.addTask(operation: perform)
            return try await taskGroup.next()!
        }
    } catch {
        switch error {
        case let error as ThrowsError: throw error
        default: throw cancellationError
        }
    }
}

This function should throw only ThrowsError type, however, when used, compiler says Thrown expression type 'any Error' cannot be converted to error type 'XXX'

There is no way to create a TaskGroup with a typed error. To emulate it, just force cast.

@Sendable
public func withSingleTaskInThrowingTaskGroup<Returning: Sendable, ThrowsError: Error>(
  perform: @escaping @Sendable () async throws(ThrowsError) -> Returning
) async throws(ThrowsError) -> Returning {
  do {
    return try await withThrowingTaskGroup { taskGroup in
      taskGroup.addTask(operation: perform)
      return try await taskGroup.next()!
    }
  } catch {
    throw error as! ThrowsError
  }
}

However, what you've got there is not actually using a task group. Switching to Task doesn't help with the problem, though, as Task doesn't support typed errors, either.

@Sendable public func taskValue<Success: Sendable, Failure>(
  operation: sending @escaping @isolated(any) () async throws(Failure) -> Success
) async throws(Failure) -> Success {
  do {
    return try await Task(operation: operation).value
  } catch {
    throw error as! Failure
  }
}

This is unsafe as a Task may also throw an error of type CancellationError independent of the body closure.

:hot_beverage:

You are not at the point. The generic function is declared (and compiled) as throwing ThrowsError, however when used, compiler ignores the exception type.

That can only happen if you use Task.checkCancellation(), in which case, typed throws is not possible.

False!

struct E: Error { }
func f() async throws(E) {
  try await withSingleTaskInThrowingTaskGroup(cancellationError: E()) {
    () throws(_) in throw E()
  }
}

Yup, you're right—Task does not currently throw CancellationError on its own and I was misremembering. Need more coffee!

I believe @hibernat is not explicitly writing throws inside the body of the closure, in which case in Swift 6 mode the compiler does not infer an error type for the closure (beyond any Error). This is by design. See the relevant section in SE-0413.

1 Like

Thanks, that is great response. However, why is this function accepted by the compiler? There is no inference needed, the exception type is declared, required is the same exception type for the perform throwing function/closure and the exception thrown by the function.

Watch this (compiler error (type is any Error)):

struct E: Error { }

@Sendable
func g() async throws(E) {
    throw E()
}

func f() async throws(E) {
    do throws(E) {
        try await withSingleTaskInThrowingTaskGroup(cancellationError: E()) {
            try await g()
        }
    } catch {
        
    }
}

From what I see locally, this fails to compile in a way I'd expect:

func f() async throws(E) {
  do throws(E) {
    try await withSingleTaskInThrowingTaskGroup(cancellationError: E()) {
      // ^ ^ ^ Thrown expression type 'any Error' cannot be converted to error type 'E'
      try await g()
    }
  } catch {

  }
}

This is because you didn't explicitly write () throws(E) in in the body of the closure you passed to withSingleTaskInThrowingTaskGroup(), which causes it to infer the type of the closure as throwing any Error, which is then incompatible with the expected type E (which is inferred from the explicit E() you passed as an argument to withSingleTaskInThrowingTaskGroup().)

If I add () throws(E) in, it compiles as expected:

func f() async throws(E) {
  do throws(E) {
    try await withSingleTaskInThrowingTaskGroup(cancellationError: E()) { () throws(E) in
      try await g()
    }
  } catch {

  }
}
3 Likes

OK. I accept this explanation, with respect to the design (SE-0413). Thanks again!

1 Like

It seems to me that you'd be better off with a generic function that works on task and groups.

func transformError<Success, Failure>(
  _ task: () async throws -> Success,
  transform: (any Error) -> Failure
) async throws(Failure) -> Success {
  do { return try await task() }
  catch let error as Failure { throw error }
  catch { throw transform(error) }
}
struct E: Error & Equatable { }
@Test func test() async throws(E) {
  await #expect(throws: E()) {
    try await transformError {
      try await Task { throw E() }.value
    } transform: { _ in E()  }
  }

  await #expect(throws: E()) {
    try await transformError {
      try await withThrowingTaskGroup { taskGroup in
        taskGroup.addTask(operation: Task.checkCancellation)
        taskGroup.cancelAll()
        return try await taskGroup.next()!
      }
    } transform: { _ in E()  }
  }
}
1 Like

As I showed above, you don't need E. You just need to let the compiler know to use typed throws. Either of the following is enough to do that:

func f() async throws(E) {
  try await withSingleTaskInThrowingTaskGroup(cancellationError: E()) { () throws(_) in
    try await g()
  }

  try await withSingleTaskInThrowingTaskGroup(cancellationError: E(), perform: g)
}
1 Like

I'm aware. :slightly_smiling_face: Whether you write throws(E) or throws(_) is not really the high-order bit here.

I disagree here only because nobody knows how this stuff works yet, and it will help to be clear about it until it's not broken anymore. I.e. when we can use no code instead of the horrible throws(_).

I highly appreciate your responses. This code does not compile. This is also "works as designed"?

struct E: Error { }

func g() async throws(E) -> Int {
    throw E()
}

do throws(E) {
    async let h = try g()
    try await h
} catch {

}

Looks like it's an issue with async let. I'm afraid I don't have more information than that. Please file a GitHub issue against Swift about it?

1 Like

Danny responded:

1 Like