Rethrows does not seem to work correctly with typed throws

I was experimenting with typed throws and found a case that intuitively seems like it should compile, but currently does not:

enum DatabaseError: Error {
    case connectionLost
}

struct Database {

    func transaction(operation: () throws(DatabaseError) -> Void) rethrows {
        try operation()
    }

    func saveUser() throws(DatabaseError) {
        try transaction {
            throw .connectionLost
        }
    }
}

Conceptually, transaction only rethrows the error produced by the closure, so it seems reasonable that the compiler could preserve the concrete error type (DatabaseError) here.

However, the code currently fails to compile.

Is this a known limitation of the current typed throws implementation, or is there a deeper design reason why rethrowscannot preserve the concrete error type?

Additionally, what would currently be considered the best approach to work around this limitation while still preserving typed errors as much as possible?

1 Like

rethrows means it only throws when the argument throws, not what it throws. Meaning this is perfectly legal:

func transaction(operation: () throws(DatabaseError) -> Void) rethrows {
  do {
    try operation()
  } catch {
    throw SomethingElse()
  }
}

Generics are the intended solution, e.g.

func transaction<E: Error>(operation: () throws(E) -> Void) throws(E) {
  try operation()
}
5 Likes

There is no longer any reason to use rethrows now that typed throws is available. You can declare a generic function that throws a type parameter type instead. Since Never conforms to Error, substituting Never for your type parameter gives you the non-throwing behavior.

8 Likes

Thanks for the clarification.

I managed to get the example compiling with the following approach:

enum DatabaseError: Error {
    case connectionLost
}

struct Database {

    func transaction<E: Error>(operation: () throws(E) -> Void) throws(E) {
        try operation()
    }

    func saveUser() throws(DatabaseError) {
        try transaction { () throws(DatabaseError) in
            throw .connectionLost
        }
    }
}

I was expecting something a bit less verbose from Swift, but it is OK.

Thanks for the explanation.

You can omit the “: Error” if you like, it will be inferred from the appearance of E in throws position.

Also the requirement to re-state the thrown type on the body of the closure is a known limitation that we would like to fix eventually.

6 Likes

Unfortunately, this isn't quite true.

If you have a function like

func takesOptionalClosure<E: Error>(_: (() throws(E) -> Void)? = nil) throws(E) {}

Then you can't call it with no parameters, because it can't infer E.

Even if you replace = nil with = (() throws(Never) -> Void)?.none, it still doesn't work.

But

func takesOptionalClosure(_: (() throws -> Void)? = nil) rethrows {}

Works just fine.

2 Likes

Similarly, if you are relying on being able to throw a different error if-and-only-if the callback throws, typed throws doesn't quite mean the same thing.

1 Like

I don't believe the error-type-as-extra-arg pattern allows you to pass no arguments, since the compiler will complain that the error type could be inferred from either argument.

More overloads does fix it, but the particular case I came across this in I would've had to duplicate dozens of functions to achieve it. It didn't feel like a worthwhile tradeoff.