Typed throws

What if just:

func wrap<E: Error>(fn: () throws E -> Void) rethrows E

became

func wrap<E: Error>(fn: () throws E -> Void) throws E

So as was said, every function and method has an implicit throws statement but when it is no declared, the compiler treats it as throws Never, so rethrows would keep its usage (and maybe future deprecation) over just throws notation.

1 Like

For generic throw, that's what I'd expect. For non-generic throw though:

func foo(closure: () throws SomeError -> ()) rethrows IntError

It's an interesting scenario here, since this would/should mean that foo Doesn't throw if closure doesn't. It also shows us an interesting difference between typed rethrow and untyped rethrows:

func foo(closure: () throws -> ()) rethrows
func foo(closure: () throws -> ()) rethrows IntError
func foo(closure: () throws SomeError -> ()) rethrows
func foo(closure: () throws SomeError -> ()) rethrows IntError
2 Likes

Yeah, I mean, I think it can be totally possible and possibly a nonsense from the developer, as no Error will be passed through the outer method :laughing:


I think the behavior obtained is totally what the developer declares in the syntax, so I don't see any issue there, but that's indeed a nice set of mix between typed and non typed.
Also, I see that it could be easily detected by the editor and raise a warning/error.

This is only then a source break, if an API-designer decides to change his function declaration. AFAIK, these source breaks are allowed.

If we add typed rethrow, we should also figure out an easy syntax for functions that simply rethrow the arguments errors. It's unfortunate that this can't be the default, but it should greatly reduce the friction.

That said, I think it could be a whole 'nother proposal. Since we can just treat current rethrows as untyped.

1 Like

But I guess this would happen to Array.map :thinking:? But good to know :slight_smile:

Maybe we could make it default:

func foo<E>(_ bar: () throws E -> ()) rethrows

already uses new features, so it wouldn’t be a source break, if foo threw E here instead of Error. This also would give back a usecase to rethrows.

We most likely need to add new functions with typed rethrows given that we couldn't reinterpret rethrows in a source compatible manner.

It may not be source breaking, but it's severely inconsistent for the interpretation of rethrows to be dependent of the argument types.

Yeah I mean...an API is even changed without a new language version, which results in a source break as well. As long as the API-designer does not adopt typed throws, there is no source break.

1 Like

IIUC the ABI issue that John raised is about deciding on the typed-throws equivalent for an untyped throws-rethrows signature today.

My point was that in Swift today, there is no function which accepts a throws T closure (since such syntax is illegal), so inferring T to be as specific as possible is not a source compatibility issue as far as I can see.

1 Like

Hmm idk.

We would have the following three cases for rethrows (Or have I forgotten something?):

func foo(_ bar: () throws -> ()) rethrows // => Error (implicit, cause it’s same as bar)

func foo<E>(_ bar: () throws E -> ()) rethrows // => E (implicit, again it’s same as bar)

func foo<E>(_ bar: () throws E -> ()) rethrows CustomError // => different from bar, thus explicitly written

Seems consistent to me... :thinking:

We could make a new rule for rethrows. Something like

rethrows throws an error of the same type as the closure argument, unless an explicit type is provided.

Yes you are right. The generic parameter you speak about is not existing now. :+1:

I was just thinking to much about updating Array.map.

Rather than updating the current methods, we could just add the generic variant ones, which I think it does not break source compatibility, because non typed throws will always use the current version of the method:

// current
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

// added
func map<T, E>(_ transform: (Element) throws E -> T) rethrows E -> [T]

That comes down to the question how we infer { throw FooError.foo } again. Because if we infer to specific FooError it would call the new map.

It would be so much simpler if Swift 6 would update current source to { throw FooError.foo } as (() throws -> ()) :smiley:

Which of those two overloads would be called by this:

try array.map {
    guard $0 > 0 else { throw CustomError() }
    return $0 + 1
}

and why?

1 Like

Consider something like this:

func bar() throws BarError

fooTyped(bar) // throws BarError
fooUntyped(bar) // throws Error

I'm quite uncomfortable that you get different error types from seemingly equivalent representation, with the same argument (bar) no less.


It's source breaking (see 20-30 posts back)

So it's

func foo<E>(_ bar: () throws E -> ()) rethrows
vs.
func foo<E>(_ bar: () throws E -> ()) rethrows E

?

We want to find a solution for this problem:

func foo(_ bar: () throws MyVeryVeryLongError -> ()) rethrows MyVeryVeryLongError
//                                                            ^^^^^^^^^^^^^^^^^^^
//                                                            |
//                                                            |
//                                                         I don't want that
1 Like

What about this

I think we need to clarify if rethrow means "throws if parameter throws" or "throws parameter error type".

I think the former is the current one. And seeing this

func foo<E>(_ bar: () throws E -> ()) rethrows CustomError

I would assume seeing this

func foo<E>(_ bar: () throws E -> ()) rethrows

to be

func foo<E>(_ bar: () throws E -> ()) rethrows Error
2 Likes