Ergonomics of `rethrows` and `try`

I write a fair number of rethrows functions. These are often generic over a protocol, and the caller passes in a closure such as a predicate or transform. (Sometimes more than one.)

In every case, the implementation of the rethrowing function either calls the closures, or passes them along to another rethrowing function, or perhaps wraps them in a larger closure before doing one of those things.

Regardless, every place where the predicate is used, that line must be marked with try. Sometimes two tries are needed on the same line, as in:

try foo{ try !predicate($0) }

These try markings are all just noise.

Inside a rethrows function, any error thrown by the predicate is expected and intended to be rethrown. Nothing is gained by writing try there.

The only exception is if the rethrowing function for some reason actually wants to catch an error from one of its arguments. I consider this to be extremely rare, and even then it is only within a do-try-catch block that writing try makes sense.

When there is no catch block, the fact that we are in a rethrows function makes the use of try superfluous. It is simply unnecessary boilerplate.

I would like to see the rules around try improved in this regard, so that in the body of a rethrowing function, uses of its closure parameters do not require try (unless they are inside a do-catch block).

• • •

As a side tangent, I also think that from a readability standpoint, closure parameters that can be rethrown should be marked something like rethrown instead of throws.

Marking the parameter throws when the function itself is rethrows seems incongruous. The whole point of rethrows is that the parameter may or may not be throwing, but the word “throws” communicates to the reader that the parameter is necessarily throwing, which is incorrect.

1 Like

Without commenting on the first portion of your post (I haven't personally dealt with writing rethrows APIs enough to have an informed opinion), I think I disagree with this characterization of throws. It's not rethrows that means "the parameter may or may not throw," it's the throws keyword itself. Since non-throws function types are subtypes of their throws counterparts, every use of throws really means "may or may not throw." Using rethrows just indicates "throws when the parameter throws, doesn't throw when the parameter doesn't."

1 Like

Sure that’s fair.

The underlying idea behind the tangent (which I don’t really want to divert this thread for) is that a rethrows functions could have some parameters whose errors it rethrows, and other parameters whose errors it catches and does not rethrow.

It would be useful to distinguish between the two, so that the compiler can prevent me from inadvertently rethrowing an error from a parameter which is not intended to be rethrown.

I would say they have the same value as try in other contexts.

The primary point of try is to call attention to the additional program flow control option for the purpose of cleanup and maintaining invariants. A function or method which rethrows should also be defining the behavior on thrown error, e.g the documentation from the in-place Array.sort(by:):

If areInIncreasingOrder throws an error during the sort, the elements may be in a different order, but none will be lost.

7 Likes