Should rethrowing be "fixed"?

I've been thinking about this for a long time, but as there are some discussions about the topic, I want to point out something I've not seen mentioned so far (which most likely just means I missed a thread… but who knows ;-).

Consider this example:

func selectiveCatch(caught: () throws -> Void, thrown: () throws -> Void) rethrows {
    try? caught()
    try thrown()
}

func thrower() throws { throw NSError() }
func nonThrower() { return }
selectiveCatch(caught: thrower, thrown: nonThrower)

The compilers reaction to this is "Call can throw but is not marked with 'try'", but obviously, this is wrong: It is impossible that a rethrow happens.
To be 100% correct, the rethrows annotation has to be applied to parameters, and not the whole function.
Probably there aren't many real examples of this behavior, and I guess the current rule is more convenient for the common case — but if we get something like Typed throws - #186 by dhoepfl, we might want to have a second look at the foundations of error handling.

6 Likes

It is impossible to know this without the body of selectiveCatch, and the syntax rules at the use site shouldn’t change because you’ve edited the implementation of a function.

2 Likes

The author knows his code (hopefully ;-) — and I started this topic because they have no way to convey this knowledge to the compiler.

If we did have typed throws, we could write:

func selectiveCatch<E1, E2>(caught: () throws E1 -> Void, thrown: () throws E2 -> Void) throws E2 { ... }

selectiveCatch(caught: thrower, thrown: nonThrower) // OK because E2 is Never

Having rethrows also cover such an esoteric use case isn’t obviously needed, so I don’t see why we’d want to improve rethrows and also pursue typed throws. Improving rethrows would make more sense as an alternative to typed throws.

This is what try! Is for.

4 Likes

I think Tino’s point is that the author of selectiveCatch should be able to specify that selectiveCatch will rethrow exclusively on its second parameter.

Something like this:

func selectiveCatch(caught: () throws -> Void, thrown: () rethrown -> Void) rethrows {
    try? caught()
    try thrown()
}

Here I’ve introduced the hypothetical keyword rethrown to indicate that a parameter participates in rethrowing. The implication is that a rethrows function will only throw an error if one of its rethrown arguments itself throws.

• • •

While we’re on the topic of rethrows, has there been any consideration of documenting or making official the “escape hatch” used by DispatchQueue.sync?

2 Likes

Mostly, I think the consideration has been to make an official escape hatch (like rethrows(unchecked) or something) so that we can fix the bug DispatchQueue.sync is employing. But that hasn't happened yet.

2 Likes

Basically yes — I didn't go as far and merely stated that the author can't annotate their functions with this information, but the only piece missing for the shift towards "should be able" is a real example where this would help users of a library (by eliminating the need to handle errors that will never be thrown).

In this scenario, try! is not a good solution, and looking at [Pitch] Rethrowing protocol conformances, there might soon be another source for errors, thus more possibilities for superfluous error handling.

The question is not "is there something wrong" (this should be clear) but rather "is it an issue in actual code"? Being forced to use try even when no errors can be thrown is certainly bad (if not, we could just assume that every call can throw…), but the issue with rethrows might be so rare that it is an acceptable compromise.