[Accepted] SE-0413: Typed throws

Hey folks,

The review of SE-0413: Typed throws ran from November 16th through December 7th.

SE-0413 has been accepted.

Most of the major questions about the proposal were worked out during the pitch phase, and review focused on a few remaining details:

Parentheses

A number of people expressed that they did not like the throws(CatError) syntax, and either suggested alternatives to the parentheses or questioned if they were needed at all. The parentheses resolve a number of possible ambiguities in the grammar with varying levels of plausibility:

  • Does f<U, T>() throws (U) -> T { ... } throw a function (U) -> T and return Void or does it throw U and return T?
  • If I name a type async, is f() throws async an async throwing function or is it a synchronous function that throws async? It's the latter, so completely different from f() async throws (and f() async throws async). [review manager's note: thanks @xwu, I hate it]

The LSG concluded that we don't love the parentheses, but we think that they're the best option we have for now. We'd be open to making restrictions on the grammar that allow us to remove them without introducing ambiguity in the future as we get more experience with how people actually use the feature.

Providing an explicit type annotation for throws

During the review, it was pointed out that it would be desirable to be able to provide an explicit error type for do/catch blocks, rather than relying on type inference. The proposal was amended to make this possible (but not required) via the syntax do throws(ErrorType).

Status of rethrows

As discussed in the proposal, the rethrows keyword is potentially made redundant by the adoption of typed throws. There are a few alternatives discussed in the proposal for how it could be repurposed or removed in the future, but we are not making any of these decisions at this time.

I'd like to thank everyone who participated in this review. Your contributions help to make Swift a better language.

Steve Canon

63 Likes

I'm happy with how the process worked on this one. It was long and noisy, but the process surfaced a whole lot of good thinking, and the proposal landed in a good place as a result IMO. Thanks to everyone who helped guide the proposal and the review process.

29 Likes

Explicitly-typed throws in Swift are the thing I’ve been waiting for for years! :tada:

2 Likes

Thanks, that is actually what many of us were waiting for for a long time!

However that was unexpected to have error type declared in parentheses instead of angle brackets as we do with generics. I could not find the discussion on that matter, but it would be very interesting to know what was the reasoning behind that decision?

This follows more closely with generic syntax, and highlights the type nature of the arguments more clearly. It's inconsistent with the use of parentheses in modifiers, but has some precedent in attached macros where one can explicitly specify the generic arguments to the macro, e.g., @OptionSet<UInt16> .

This explanation is not very detailed, it tells about intuitiveness and even gives an example of angle brackets usage in macros. It is also very hard to confuse throws with other modifiers because of its position.
At the same time it seems that parentheses provide much less readability and makes more visual noise, since it's very easy to confuse this declaration with function arguments when reading code quickly.

There's not a lot to say; parentheses are just more consistent with the rest of the language. This was not at all controversial among the language study group. I understand why someone with a C++ background might prefer it, but it's just generally at odds with the way type arguments are handled in Swift.

2 Likes

one way to think about it is that angle brackets declare type parameters, but the types that go in throws(...) are just plain old types.

3 Likes

I personally like the symmetry here with the types inside the parentheses for the parameters list too.

1 Like

I was almost about to ask something similar but I remembered that the angle brackets usually signal a generic type list. In case of typed throws this would not be true unless the generic error type is declared in the leading angle brackets. Therefore they do not fit, which also implies why normal parenthesis suit better here.

3 Likes

I was looking at throws keyword more like at an attribute that is generic itself. Just like you could specify function argument type or return value type:

func foo(arg: Bar<Int>) -> Baz<String>
func foo<T>(arg: Bar<T>) -> Baz<Qux<T>>

that would naturally look the same with throws not as a declaration, but as a specification of generic type:

func foo<T>(arg: Bar<T>) throws<MyError<T>>

At the same time using parentheses looks more like a second argument list, or another function declaration that an author forgot to move to a new line:

func foo<T>(arg: Bar<T>) throws(MyError<T>)

I understand your point, yet I think the actual error type is not generic and therefore angle brackets seem out of place, to me at least.

5 Likes

Funny, now that the syntax is fixed, people will get accustomed to it and have their own “pictures” in their heads, for me the round parentheses look like an additional remark (the same way they are used in normal text): “Look, it can throw something, and by the way, this is what it then throws.”

That’s why I preferred the angle brackets as well. You can’t usually take the name of a type directly as a parameter. I would have expected to see throws(SomeError.self) with the regular parenthesis.

3 Likes

In that case it would mean that generic type is passed as a value. However a throwing function is simply a function with two different return paths, the regular return type and the error type. Therefore it’s part of the signature and isn't a parameter, hence no need for .self or .Type.

4 Likes

… and next (hopefully) are sum types :+1:

4 Likes

Wouldn’t throws Type (without parentheses) be more in line with the syntax we have today for other key words that appear at the end of a function?

Eg.

fun foo() where Self == Equatable

On a side note, will this work with generics?

For example:

struct MyStruct<MyError: Error> {
    fun foo() throws (MyError) {
    }
}
1 Like

The syntax was heavily debated over the very long course of the review process. I doubt the language workgroup is going to reopen the discussion at this point.

4 Likes

This is directly addressed in both the proposal text and the acceptance (this thread, "Parentheses").

Try it yourself: Compiler Explorer (Yes, it works, but you'll have to constrain MyError to something that refines Error in order to actually be able to construct one to throw, because Error requires neither and initializer nor static members. Here's a worked example.)

4 Likes

Not necessarily:

struct ErrorHolder<T: Error> {
  private var error: T?

  mutating func gimmeAnError(_ error: T) {
    self.error = error
  }

  func checkError() throws(T) {
    if let error { throw error }
  }
}
2 Likes

New member here; very interested in this proposal. Has the discussion continued elsewhere since the end of last year?

Hi @cadnza, as you can see from the title of this thread, we have announced that this proposal is accepted. You can try it out for yourself (see swift.org for download instructions)!