How do you call an untyped throwing overload from a typed one?

This situation is a common occurrence with typed throws. Is there an intended solution, or has not enough thought and effort been put into it yet?


A function may throw an error.

enum FunctionsError: Error { }

The function accepts a closure, and may also throw that closure's error. This cannot be modeled in the type system, so must utilize untyped throws and documentation, like we used to have to do with everything.

/// - Throws: `Error`, `FunctionsError `
func f<Error>(_ closure: () throws(Error) -> Void) throws {
  try closure()
  throw FunctionsError()
}

The problem is, when Error is Never, throws does not "automatically become" (à la rethrows) throws(FunctionsError), as it is then statically known to be. Casting works fine, if you add in some spelling difference so it's not a real overload…

func f(neverThrowing closure: () -> Void) throws(FunctionsError) {
  do { try f(closure) }
  catch { throw error as! FunctionsError }
}

…and the compiler does not consider the proper spelling to be an invalid redeclaration…

func f(_: () -> Void) throws(FunctionsError) { }

…but what can you put in the body to call the more versatile overload?

(My guess is that this is impossible, and warrants at least a macro, to avoid complete copypasta.)

Have you considered adding a case (with payload) to FunctionsError to wrap the closure's error in, instead of going all the way to throwing an any Error?

Something like this perhaps:

enum InnerError: Error {}
enum OuterError: Error {}

func f(_: () throws(InnerError) -> ()) throws -> () { print("any Error overload") }
func f(_: () -> ()) throws(OuterError) -> () {
  print("more specific")
  do {
    let fn: (() throws(InnerError) -> ()) throws -> () = f
    try fn {}
  } catch {
    throw error as! OuterError
  }
}

try f {}

Yes. I think there's room for the language to support such unions better in the future—people have some good ideas about them—perhaps if Never as associated values made cases not need to be checked…? I don’t see a related solution at present. FunctionsError can’t actually be assumed to be tied to the function, or a closure.

That's …one error type? That overload has to account for all errors that are not Never, not one specific type in particular.

Looks nice and straightforward to me:

enum OuterError<Inner: Error>: Error {
  case fooError
  case closureError(Inner)
}

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

Also it appears that exhaustivity checking is smart enough to ignore the uninhabited case:

func f(_ e: OuterError<Never>) {
  switch e {  // ok
  case .foo: print("Hi")
  }
}

Ah I see, yeah, there might not be a way to call the other overload in this case. However if you use typed errors instead of any Error, you don't need the second overload, so that's another argument in favor of that approach.

1 Like

While I was editing with this idea…

…you demonstrated that it's actually almost possible. Thanks!

What doesn't work yet, is do/catch, even disregarding uninhabitability:

enum E: Error { case a }
do throws(E) {
  throw .a // Error is not handled because the enclosing catch is not exhaustive
} catch .a { }

If that can be fixed, maybe your OuterError belongs in the standard library as the canonical error union type.

@frozen public enum ErrorUnion<_0: Error, _1: Error>: Error {
  case _0(_0)
  case _1(_1)
}

Just needs sugaring to allow for

func f(_ e: ErrorUnion<E, Never>) {
  switch e {
  case .a: break
  }
}

instead of

case ._0(.a): break