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'

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.

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

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

(post deleted by author)

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

Ran into this issue and though I understand the closure signature requirement, it still feels like a bug.

Case in point, consider the following:

struct FooError: Error { }

func a(_ block: () throws(FooError) -> Void) { }
func b(_ block: @autoclosure () throws(FooError) -> Void) { }

func c() throws(FooError) { }

a { try b() } // ⚠️ compiler error
a { () throws(FooError) -> Void in try c() } // ✅ works
a { () throws(_) -> Void in try c() } // ✅ works
b(try c()) // ✅ works

TL;DR - making the closure an @autoclosure produces the desired outcome of the error type being inferred (func b). The expectation is that func a would have the same inference behavior but it does not, as already discussed.