Call to rethrow functions inside typed throw

Compiler cannot infer rethrow error type when calling rethrows function inside throws(e)

The rethrows telling us “Error type thrown is the same passed closure throws”.
But in this simplified example in some reason it cannot infer that body error type is exact, not any Error.

What I’m missing?
Are there suggestions on using rethrows functions (like DispatchQueue.sync) inside typed-throw ones.

func rethrowFn(_ body: () throws -> Void) rethrows {
    try body()
}

func typedThrowFn<E: Error>(_ body: () throws(E) -> Void) throws(E) {
    // Error: Thrown expression type 'any Error' cannot be converted to error type 'E'
    try rethrowFn {
        try body()
    }
}
func rethrowFn(_ body: () throws -> Void) rethrows {
    try body()
}
func rethrowFn<E: Error>(_ body: () throws(E) -> Void) throws(E) {
    try body()
}

If E is inferred to be Never, you don't need try at call site, in effect making both of them non-throwing, when passing a non-throwing argument. Is this not working for you?

func rethrowFn<E: Error>(_ body: () throws(E) -> Void) throws(E) {
    try body()
}

func typedThrowFn<E: Error>(_ body: () throws(E) -> Void) throws(E) {
    try rethrowFn(body)
}
func untypedThrowFn(_ body: () throws -> Void) rethrows {
    try rethrowFn(body)
}
func nonThrowingFn(_ body: () -> Void) {
    rethrowFn(body)
    // ^^ no try
}
func nonThrowingCallsRethrowsFn(_ body: () -> Void) {
    untypedThrowFn(body)
    // ^ note this one is rethrows
}
2 Likes

I believe this is because rethrows doesn’t technically require that the error thrown is the same error the closure threw, just that the error is not thrown if the closure doesn’t throw. In other words, this is legal:

func f(cb: () throws -> Void) rethrows {
  do {
    try cb()
  } catch {
    throw SomeOtherError()
  }
}

If I know this isn’t what the function does, I often wrap it like so:

do {
  try someThrowingFunc()
} catch {
  throw error as! E
}
3 Likes

It's like that, but more specifically, that the only two types that can be thrown from rethrows are Never and any Error.

I.e. the normal solution with typed errors cannot save you:

func typedThrowFn<E>(_ body: () throws(E) -> Void) throws(E) {
  try rethrowFn { () throws(E) in // Thrown expression type 'any Error' cannot be converted to error type 'E'
    try body()
  }
}

Normally, this means that rethrows is not helpful anymore, because typed throws can handle both of those, as a subset. But it still has a place. There is no way to represent the following with typed throws. rethrows can do it, though:

Throw a type-erased version of one of multiple error types, or throw Never, and therefore don't require try.

func f<Error1, Error2>(
  _ f1: () throws(Error1) -> Void,
  _ f2: () throws(Error2) -> Void
) rethrows {
  try f1()
  try f2()
}

I don't think there's a way to add typing to known errors with less syntax than an operator and .self.

func typedThrowFn<E>(_ body: () throws(E) -> Void) throws(E) {
  try rethrowFn(body) ¿! E.self
}
¿!
infix operator ¿!

public func ¿! <Value, Error>(
  value: @autoclosure () throws -> Value,
  errorType: Error.Type
) throws(Error) -> Value {
  do { return try value() }
  catch { throw error as! Error }
}
3 Likes

Strictly speaking, this can be spelled:

func f<E>(
  _ f1: () throws(E) -> Void,
  _ f2: () throws(E) -> Void
) throws(E) {
  try f1()
  try f2()
}

func f<E1, E2>(
  _ f1: () throws(E1) -> Void,
  _ f2: () throws(E2) -> Void
) throws(any Error) {
  try f1()
  try f2()
}

Sort of.

  1. Swift doesn't have official code generation tools, so I wouldn't consider them the same from a maintenance point of view.

  2. You've added the overload where the signature preserves a typed error thrown by both closures. But this does not handle these useful overloads where one closure throws Never, which should allow error type preservation as well. Swift has no mechanism for this aside from explicit overloads.* (This is why I haven't bothered ever incorporating matching error types, rather than use rethrows.)

func f<E>(
  _: () -> Void,
  _: () throws(E) -> Void
) throws(E) { }

func f<E>(
  _: () throws(E) -> Void,
  _: () -> Void,
) throws(E) { }
  1. Only your first overload is necessary, because you can cast a function that uses one typed error to one that uses multiple. :face_with_spiral_eyes:
func f<E>(
  _: () throws(E) -> Void,
  _: () throws(E) -> Void
) throws(E) { }

extension Error {
  func `throw`<Never>() throws(Self) -> Never { throw self }
}
struct E1: Error { }
struct E2: Error { }

let specializedF1 = f as (
  () throws(E1) -> _,
  () -> _
) throws -> _
try specializedF1(E1().throw) { } // Compiles.
try (f as (_, _) throws -> _)(E1().throw) { } // Compiles.
try f(E1().throw) { } // Type of expression is ambiguous without a type annotation

let specializedF2 = f as (
  () throws(E1) -> _,
  () throws(E2) -> _
) throws -> _
try specializedF2(E1().throw, E2().throw) // Compiles.
try (f as (_, _) throws -> _)(E1().throw, E2().throw) // Compiles.
try f(E1().throw, E2().throw) // Type of expression is ambiguous without a type annotation

* The specialized / cast functions have type-safe parameters, but the E becomes any Error, as you might expect, when multiple non-Never error types are present. And E is the non-Never error type, in the other case. It would be nice if this could become a "real" feature. But then again, I guess that goes for typed throws in general. :pensive_face: