While there is an active discussion of typed throws over in this thread, the latest proposal is from a thread started in July 2020. We have an updated proposal with a revised design for typed throws, incorporating a number of the ideas and arguments that have come up over the years. I'm also working on an implementation of this design.
The basic idea is unchanged: functions and closures can specify a thrown error type, so that clients know what kind of error they need to handle when calling that function:
func callCat(name: String) throws(CatError) -> Cat { ... }
func feedKitty(name: String) {
do {
let cat = try callCat(name: name)
cat.feed()
} catch {
print("Cat wouldn't come: \(error.catReason))") // error is of type CatError
}
}
The introduction of typed throws has significant impact on the type system and standard library, all covered by proposal document.
I think the motivation section could be much clearer on when typed throws are meant to be used instead of untyped throws, and vice versa. As written, it seems like it’s a mostly aesthetic choice (“Some developers are not satisfied”), which doesn’t really square with Swift’s clear stance so far that untyped throws are enough for the vast majority of cases. Either this is a repudiation of that stance, or it’s a carve-out meant for very specific situations like embedded. It would be good to have clarity on what the intent is here. (Using more realistic examples instead of callCat would also help here.)
It also feels to me like a more natural spelling would be throws SomeError rather than throws(SomeError). Is there a reason the parentheses are needed?
As your upper layer just throws without any type on it, it wouldn't need another nested enum, in fact, I think the purpose is not to create nested enums to encapsulate downwards thrown errors but use try ... catch statements in the immediately upper layer in order to eat the error and stop throwing if possible, but you can do typed throws if you think it makes sense to you, it all ends with a correct design.
Would one be able to catch thrown any Errors from called APIs and “translate” them into concrete typed throws? e.g. If the Cat API had some remote cats, could I catch the URLErrors I might get from a remote request and throw a typed CatError instead?
If so, will the compiler know that consumers of my API thinking they could catch URLErrors for remote operations are wrong and warn them that only CatErrors will ever be emitted when they attempt to catch an impossible error?
Yeah, that’s what I thought. I think it would be really unfortunate and kind of point-defeating, if it’s not possible to exhaustively catch the errors from a do-catch except in the easiest of situations.
I think the proposal should explicitly state whether catching exhaustively all the possible thrown errors lets you omit a final catch clause or not. It is somewhat inferred from the wording, but having to do this is counterintuitive:
do {
try callCat() // throws CatError
try callKids() // throw KidError
} catch let error as CatError {
} catch let error as KidError {
} catch {
// 1. why is this catch-all still needed?
// 2. will you get a warning about it being unreachable?
}
When we started on an implementation when the last pitch came up, we noticed that throws SomeError introduces some parsing ambiguities, because of contextual keywords. Here is an example that would be ambiguous to parse:
Here we could either parse func bar() throws (mutating) and func baz(), or func bar() throws and mutating func baz(). I’m sure we could come up with some rules to disambiguate, but we thought at the time that it would be easiest to avoid ambiguities altogether. If more people called for the throws SomeError syntax though, it would be possible to rethink that for sure.
I think the decision to avoid tumbling down the sum type rabbit hole is correct, and I don't mind that "error handling case exhaustion checking" might not be as smart as it could. At least the rules are clear: It's either the one concrete type, or any Error.
While I welcome the possibilities it brings for avoiding existentials (eg: for embedded) and more expressive APIs, I am most excited about closing the gap to types like Result and Task and the generalizations it brings to their implementations.
What I am really, REALLY excited about is this:
public protocol AsyncIteratorProtocol<Element, Failure> {
associatedtype Element
associatedtype Failure: Error = any Error
mutating func next() async throws(Failure) -> Element?
}
public protocol AsyncSequence<Element, Failure> {
associatedtype AsyncIterator: AsyncIteratorProtocol
associatedtype Element where AsyncIterator.Element == Element
associatedtype Failure where AsyncIterator.Failure == Failure
__consuming func makeAsyncIterator() -> AsyncIterator
}
Getting primary associated types on AsyncSequence (combined with an improved stream API) will be fantastic! Swift 6 world domination tour, let's gooo! ; )
That would be a source-breaking change (because right now both variables and types can be named mutating). Also there’s probably other contextual keywords with similar arising ambiguities.
Yeah, that’s one of the main selling points of this proposal for me too. Let’s hope that we can somehow get this into the Standard Library in an ABI-stable manner.
Looks good overall. Just a couple quick questions.
Will it be possible to call functions that throw a non-Never uninhabited types without using try despite them not being considered non-throwing in the type system?
Did you investigate whether throws E is viable in the grammar and if so did you consider omitting the parentheses in throws(E)?
This should be in the alternatives considered, including the part that says it could be reconsidered if people feel strongly. Reviewers should be aware of that.