Typed throw functions

Yes, that makes sense to me.

That's sounds complicated. :-) Why not just make this an error:

func bar() throws {...}
func foo() throws SomeType {
  try bar() // thrown thing is not necessarily SomeType!
}

We need an answer to this, but you would be able to write it long hand, and we can add a method somewhere to sugar this:

func foo() throws SomeType {
  do {
    try bar() // thrown thing is not necessarily SomeType!
  } catch let e : SomeType {
    throw e
  } catch {
    fatal error or whatever
  }
}

-Chris

5 Likes

Sounds good to me. It would be really great if we could find someone who would volunteer to build a proof of concept of this language extension. This would drive the topic forward as we could start plying around with this feature and look for weaknesses and what we might need to improve.

We almost finished the pitch, which will be very similar to the proposal for review, that would give us some Swift coders feedback about to where and how to start implementing this one.

This is not covered by the proposal and that imply a compiler error for now. But this is the first iteration, we should keep our feet on the ground.

1 Like

This will limit the possibilities of the language in comparison to structural typing like its supported in TypeScript. As far as I can see its also not completely orthogonal if you are thinking about the multi error error scenario in one do block. Having this example

func callKids() throws KidsError -> Kids
func callCat() throws CatError -> Cat

do {
    let kids = try callKids()
    let cat = try callCat()
} catch {
    // `error` is just `Swift.Error`
    // do some pattern matching with the error which can be in conflict with future type inference
}

It might be a conflict to later switch the type inference behaviour of the catch clause to let's say infer something like KidsError | CatError.

But I totally get it that a lot of people thought about | and it's not going do be part of the language. So we can propose to infer Swift.Error as far as I can see.

1 Like

But maybe I'm just completely wrong here. Can someone think of a case where inferring a more specific error type in a later update of the compiler will lead to problems regarding compatibility?

It should not be a problem as far as I can see, because a specific error like CatError has all properties of its protocol i.e. Swift.Error so it can stand in for it. :thinking:

extension Error {
    func test() {
        print("Hello")
    }
}
struct CatError: Error {
    func test() {
        print("World")
    }
}
do {
    throw CatError()
} catch {
    error.test() // prints "Hello" when inferred as `Error`, "World" if inferred as `CatError`
}
5 Likes

I think that there's a huge difference between the example and what's being proposed and here's why:

extension Error {
    func test() {
        print("Hello")
    }
}
struct CatError: Error {
    func test() {
        print("World")
    }
}

// Now:
func foo() throws { throw CatError() }

do { try foo() } catch { /* Inferred as Error */ }

// Proposed:
func foo() throws CatError { throw CatError() }

do { try foo() } catch { /* Inferred as CatError */ }

func bar() throws { throw CatError() }

do { try bar() } catch { /* Inferred as Error */ }

Not proposed nor scope of the proposal:

do {
    throw CatError() // No information about the **function signature**, no inference applicable
} catch {
    error.test() // prints "Hello", can be casted to CatError if desired.
}

If there is no concise information about the error being thrown and this is only achieved using the throw Type information from the function signature. The behaviour will remain unchanged and hence, totally backwards compatible.

Sure, but you he didn't ask about what is being proposed. You He asked about "inferring a more specific error type in a later update".

If you don't change the inference, then obviously nothing will break from changing the inference.

1 Like

Thanks a lot for this example! It opened up some problematic points regarding inferring the type in a general catch clause and keeping source compatibility. Very helpful. :+1:

1 Like

Sorry for confusing you and minuscorp :P

3 Likes

That's fine :stuck_out_tongue:

1 Like

So for your information: We are going to have the draft done by mid of next week.

So, we probably want to maintain src compat:

do {
  try throwUntypedError()
  throw FooError.error
} catch {
  // error is Error
}

and the pitch wants to add handling on typed throws

// ok, exhaustive check
do {
  try throwFooError()
} catch let error as FooError {
}

There're a few cases I'd like to clarify. Do any of the below compile, and if so, what would their behaviours be?


When functions throw different types and we catch both of them:

// Is this exhaustive?
do {
  try throwFooError()
  try throwBarError()
} catch let error as FooError {
} catch let error as BarError {
}

What if we don't annotate the catch block:

do {
  try throwFooError()
  try throwBarError()
} catch {
  // What is `error` type?
}

When function mix throwing functions, and throw syntax, especially when the thrown errors are the same type

// Is this exhaustive?
do {
  try throwFooError()
  throw FooError.error
} catch let error as FooError {
}

and the with unannotated handling:

do {
  try throwFooError()
  throw FooError.error
} catch {
  // What is error type
}

This is discussed above: when there are only throw statements:

do {
  throw FooError.error
  throw BarError.error
} catch let error as FooError {
} catch let error as BarError {
}

It is unintuitive to have throw syntax be untyped by default. We still need typed throws when throwing from inside the function with typed error. Now we'd have both typed and untyped throws with virtually no way to distinguish between the two.

What if instead have all throw statements be typed, but don't explicitly infer it in the catch block. In which case, we only use typed error for exhaustivity checking.

do {
  throw FooError.error
} catch {
  // error is `Error`
}

// OK - is exhausitive
do {
  throw FooError.error
} catch let error as FooError {
  // error is `FooError`
}
2 Likes

If we cannot rely on inferring, could we rely on the editor suggestions? Autocompletion relying on the different throw types in the do statement? The rest of the questions I think they're in the proposal document that we're working on if you want to check it out

If only we can pin this so I don't need to look up 30 comments above :thinking:

I've added a link to the first post, but I cannot pin certain messages inside the thread I think!

I mean sure, just want to clarify and see the soundness of the system if I can make sense with only trusty text editor. I mean, the compiler gotta do something.


There's still this one I don't see, rather. It seems to imply that the first one fails the exhaustivity since throw is untyped, and the second one infer error to be Error (Scenario 5 for both). Feels like they should at least be called out.

We were afraid of breaking Source Compatibility with the next example:

extension Error {
    func test() {
        print("Hello")
    }
}
struct CatError: Error {
    func test() {
        print("World")
    }
}
do {
    throw CatError()
} catch {
    error.test() // prints "Hello" when inferred as `Error`, "World" if inferred as `CatError`
}

And we don't want to break source compatibility. So one path was to just infer the type when used in functions (i.e. func foo() throws FooError) but no changes when calling throw directly (i.e. throw FooError) just for maintaining source compatibility. But maybe we should drop the magic catch and rely on the compiler to gather all the possible throwing types into the do statement and use autocompletion to suggest each of those errors being gathered. Thoughts?

1 Like

My case already cannot be old source, since the throwFooError() has typed throw. I definitely agree that blocks with only throw statements should maintain old behaviour.

What's a hard sell for me is that the blocks with only typed-throw functions get new inference behaviour. So there's a line drawn somewhere between old Error behaviour and the new FooError behaviour, which I don't think is intuitive. Especially if adding throw statements with the same error type causes the exhaustivity check to fail.