Typed throws

So it seems that option B is not there way to proceed even as one said, it is wieird not to control the type of the error bring rethrown, butt maybe this proposal is not the place for that

The meaning of rethrows.

rethrows doesn't mean "only throws the errors the passed-in function throws", it means "only throws if the passed-in function throws". The above code compiles just fine, and the signature is correct.

The purpose of rethrows is to ensure that if you pass a closure to a rethrows function and the closure does not throw, you are not forced to write try around the outer function. It signals the circumstances in which a throw may happen.

1 Like

I understand that, but it still bends my mind as I kinda focused on the idea of () -> Void meaning () throws Never -> Void. Would that even require rethrows to still need to exist?

I guess I need to re-read the previous thread to find the details why rethrows can't be typed.

@lukasa am I correct at least on the part that functions which are known to be of kind B and currently use rethrows could be adjusted in a typed throws world where () -> Void means () throws Never -> Void to use throw E where E is the error from the closure.

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

// after
func map<T, erased E: Error>(_ transform: (Item) throws E -> T) throws E -> [T]

If you pass a non-throwing function (which is then known to throw Never) the compiler won't require try from you. It preserves the same effect as rethrows.

1 Like

Bikeshedding alarm: Could rethrows be replaced through throws?(E)?

public enum MyLibraryError: Error {
    case errorProcessingHashedData(baseError: Error)
}

public func withHashedData<ReturnValue>(
_ input: Data, _ body: (SHA256Digest) throws -> ReturnValue
) throws?(MyLibraryError) -> ReturnValue {
    let hashed = SHA256.hash(data: input)
    do {
        return try body(hashed)
    } catch {
        throw MyLibraryError.errorProcessingHashedData(baseError: error)
    }
}

It would preserve the throwing if the closure thrower, but would expose the custom error directly. If the closure is non throwing, it won't throw its own type.

In other words this is basically a typed rethrows.

I really like that we go into interesting detail discussions, but just to give this discussion direction and structure, can we first clear up some determining factors?

Can someone explain to me, what changes to the language are in general allowed at all at this stage of the language?

  • Is source compatibility required no matter what? If this is the case, we will have a hard time with this proposal I guess. Concentrate on section "Source Compatibility" first as I already mentioned.
  • Is ABI stability required no matter what?

To me this is important, because it determines the options we have at all and we can fail on nasty details fast here without needing a discussion for the other things (though I really like the ideas you come up with :+1:).

thx :slight_smile:

The most recent answer to your question is the "Principles for Evolution Proposal Evaluation" chapter of Principles for Trailing Closure Evolution Proposals

Is source compatibility required no matter what? If this is the case, we will have a hard time with this proposal I guess.

Source compatibility is the first of those principles, so I agree with your conclusion. This pitch won't easily turn into a proposal.

Is ABI stability required no matter what?

Of course it is. You can't break existing devices. But you can evolve the language so that typed throws is only available from a specific Swift version and specific system version (compare with some View which is only available on some version of the systems that rely on ABI stability).

2 Likes

Exactly what I was looking for. Thx!

This was my conclusion along the last days writing on the pitch. Bringing more information into the error types making them more specific in combination with type inference leads to a situation where error types change automatically in existing code (getting more specific). So as long as we are not auto converting source code (which I guess is not an option anymore) to explicitly declare the existing errors to be of type Swift.Error, I see no way to bring this into the language.

Other options?

1 Like

Well, start to see what happens when you do not automatically change error types in existing source code?

Maybe you'll end up with a perfectly viable language with useful new features, and that will be cool. Maybe you'll end up in a dead-end, but the community will have a better sense of what's to fix.

Anyway, the goal is not to tame the language so that it is like you want it to be. There are many flavors of "typed throws", and the game is to find the one that suits Swift the best.

2 Likes

And that's the whole purpose of this thread. There're some ideal cases where typed throws could be applied into the language and be useful, but the break source compat. The base idea we're proposing does not break source compat at first analysis, but it is more limited about what could it do breaking it.
I'm eager to see more ideas and helping us polishing the raw edges of the pitch to make it into a proposal soon.


Having some help on implementing the basic behavior (syntax) it would be very useful for us to see what can and what can't be done. Where should I start? On swift-syntax?

I wan't to reduce potential risks as early as possible. So to be concrete and keep my motivation going, I have no solution keeping source compatibility for this

struct Foo: Error { ... }
struct Bar: Error { ... }
var throwers = [{ throw Foo() }] // Inferred as `Array<() throws -> ()>`, or `Array<() throws Foo -> ()>`?
throwers.append({ throw Bar() }) // Compiles today, error if we infer `throws Foo`

If someone has, please share! :slight_smile:

You answered your own question:

// Compiles today, error if we infer throws Foo

So don't infer throws Foo, and infer throws Error instead. This will preserve source compatibility.

When one wants an array of functions throwing Foo, they'll write:

var throwers: [() throws Foo -> ()] = [{ throw Foo() }]
3 Likes

For me, I think I told you so. throws functions should never be inferred to be the typed throw but the base throws in collections (array, set), unless the type is specified as follows:

struct Foo: Error { ... }
struct Bar: Error { ... }
var throwers: [() throws Foo -> Void] = [{ throw Foo() }]
throwers.append({ throw Bar() }) // error: type mismatch
1 Like

To summarize: as of today we have two different cases of rethrows.

1:

func foo(_ bar: () throws -> ()) rethrows {
    try bar()
}

2:

func foo(_ bar: () throws -> ()) rethrows {
    do {
        try bar()
    } catch {
        throw CustomError(base: error)
    }
}

Both of these should continue to work exactly as they are now, without any source code changes required.

Maybe I'm overseeing something, but I will just write my idea nonetheless.


If we had a typed throws, we would need a typed rethrows as well.

Our two cases, that we have today would work exactly the same, as they worked in the past. I repeat them here with the implicit types in comments.

1:

func foo(_ bar: () throws /* Error */ -> ()) rethrows /* Error */ {
    try bar()
}

2:

func foo(_ bar: () throws /* Error */ -> ()) rethrows /* Error */ {
    do {
        try bar()
    } catch {
        throw CustomError(base: error)
    }
}

AFAIUI, this is the pre-typed-throws-behaviour.

Now we want the same but with typed Errors as well. So we have the following additional cases:

3 (typed version of (1)):

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

// If every function implicitly threw Never, we could write
func foo<E>(_ bar: () throws E -> ()) throws E {
    try bar()
}
// which would semantically be the same

4 (typed version of (2)):

func foo<E>(_ bar: () throws E -> ()) rethrows CustomError {
    do {
        try bar()
    } catch {
        throw CustomError(base: error)
    }
}

I cannot think of a reason ATM, why this should not work.

6 Likes

I don't see a reason why your examples with typed version of rethrows should not be source compatible :thinking:

And If that works, we would have another choice:
Would this:

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

be:
A: equivalent to:

func foo<E>(_ bar: () throws E -> ()) rethrows Error { … }

B: equivalent to:

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

C: or invalid.

1 Like

Ok right. But without my knowledge about the history of Swift, I would be surprised about

let some /* : () throws -> () */ = { throw FooError.foo }

not being

let some /* : () throws FooError -> () */ = { throw FooError.foo }

Isn't it somehow the default to infer the most specific type possible? :thinking:

But it's good to have this option.

I think it would be feasible to have a rule of the form:

func foo<T>(_: () throws T -> ()) { ... }
let _ = { throw Foo.error } // inferred as '() throws -> ()'
foo({ throws Foo.error }) // inferred as '() throws Foo -> ()'

That is, when there's no context we default to Error, but if there's a generic parameter to bind the throws type to, we allow more specific inference.

It also seems fairly likely to me that the source break wouldn't be that large (unless there's a more common use-case I haven't thought of?). It might make sense to tighten up the inference behavior as a change in Swift 6, where the Swift 5 compatibility mode would continue to infer the throws type as Error.

5 Likes

How would that behave in a do block like

do {
    throw Foo.error // statement infererred as Error instead of Foo?
} catch let error as Foo {
    // would that catch?
}
// can we still check exhaustiveness?

How is catch working internally? Does it need the inferred specific type at this point?

The current behavior tries to match the closest match possible available, so it would go through Foo

If we can show that this is managable for Swift users, that would be clean and consistent. <3