It occurs to me that we could allow typed throw without preventing evolution of the error type. For instance, this declares a typed throw:
func fetch() throws(NetworkError) { ... }
And because of this, fetch can only throw errors of this type. But that doesn't necessarily imply that at the call site catching NetworkError is enough. If we assume the type can change at a later time, we still need a catch-all clause to make sure no other errors have been thrown to handle them:
do {
try fetch()
} catch let error as NetworkError {
...
} catch let error {
...
}
This solves the problem of making the ABI around thrown errors more efficient without changing the language model. That said, it'd be nice to know this last catch is a fallback that isn't expected to be triggered, so we could add a mechanism similar to switching on non-frozen enums, with @unknown in the last catch:
do {
try fetch()
} catch let error as NetworkError {
...
} @unknown catch let error {
...
}
Here @unknown catch would emit a warning for any declared typed throws not handled by the previous catch clauses. It's a warning though, it won't stop code from building if the typed throw becomes untyped or changes to another type.
In other words, with @unknown catch you can be exhaustive at the call site, but you don't have to (just don't use @unknown).
This could be extended more generally by allowing more types in typed throws, even when it has no ABI or performance benefit:
func fetch() throws(NetworkError, JSONError, *) { ... }
// Here the * denotes that the function is able to throw anything,
// but the two first types are "worth" checking for.
And if you're using @unknown catch the compiler would warn you about any types comming from typed throws unhandled by previous catch clauses:
do {
try fetch()
} catch let error as NetworkError {
...
} @unknown catch let error { // warning: missing catch for JSONError
...
}
So the benefits are:
- At the ABI level, single-type typed throws can skip the existential box (except at the boundary of resilient libraries because evolution is allowed to change the type).
- At the call site, you can use
@unknown catchand let the compiler tell you about the error types "worth" checking. There's no pretence those are the only types however, hence the trailing@unknown catchfor handling less expected errors.
But if you don't use @unknown catch, you are free to ignore that typed throw is a thing that exists.
Future direction: I suppose we could add "frozen" typed throws, like frozen enums, which could allow exhaustive catching without the need for @unknown catch at the end. That could be a liability however, so I don't know.