Thanks, Doug, all very helpful. (And thanks for the bug fix!)
My line of questioning came from thinking about what people familiar with Java and similar languages will expect of this new feature. (I suspect expectations imported from Javaland are one of the origins of community support for typed throws.) There will be a natural temptation to do the Java-like thing of having multiple catch clauses organized by type, moving error handling into a large, contiguous chunk of code:
try {
// bunch of code here
} catch(FileNotFoundException e) {
// prompt for different file
} catch(IOException e) {
// retry after timeout
} catch(LogicException e) {
// display logic error to user
}
Both (1) the fact that do blocks in Swift compute a single type bound for all the errors thrown within and (2) the fact that is/as defeat exhaustiveness checks mean that the Java-like pattern above is a Swift anti-pattern (if it wasnât already), and more local error handling is the way:
do {
// just the I/O code
} catch is FileNotFound {
// prompt for different file
} catch { // other generic I/O error
// retry after timeout
}
do {
// just the logic piece
} catch { // only LogicError
// display logic error to user
}
Itâs a better way, IMO, but it will certainly thwart some expectations. Getting from the above to the below is going to beâŠa journey for many people. Hopefully the docs will set a good precedent.
To be clear: still very much +1 on the proposal and the late do throws(âŠ) addition to it.
Would it be possible to support a do throws(_) syntax? This would be a way to enable the Swift 6 âthrows inferenceâ behaviour in Swift 5? This would be nice to be able to get this without needing to extract any typed-throwing code to its own function.
The proposal already provides a FullTypedThrows upcoming feature flag to enable the Swift 6 'throws inference' behavior in Swift 5. I'd rather not also add a syntax to the language for it, which would become redundant in time.
This promotes a coding style where each error type has its own do-catch block, e.g.
func foo() throws(FooError) -> Foo {}
func bar() throws(BarError) -> Bar {}
do {
try foo()
} catch {
// error is FooError here
}
do {
try bar()
} catch {
// error is BarError here
}
which is fine, I guess.
However, when we need to transfer a value between these different do-catch blocks, we have to deal with DI again, as do is not yet available as an expression:
func foo() throws(FooError) -> VeryLongFooReturnType {}
func bar(_ foo: VeryLongFooReturnType) throws(BarError) -> Bar {}
let fooResult: VeryLongFooReturnType
do {
fooResult = try foo()
} catch {
// error is FooError here
}
do {
try bar(fooResult)
} catch {
// error is BarError here
}
As this style of error handling seems to be very encouraged through the typed throws design, I hope that there will be a relatively quick followup to get do expressions into the language.
Still, I want to express my opinion that this ought to work (at least when it's as simple to check exhaustiveness as it is here; I can see why we wouldn't necessarily want to do exhaustiveness checking as complex as switch does for do-catch):
do {
let fooResult = try foo()
try bar(fooResult)
} catch is FooError {
// error is FooError here
} catch is BarError {
// error is BarError here
} // do-catch is exhaustive, no catch-all needed
i donât really see how Never is a special case here, it just has an ordinary conformance to Error like any other type could have. after all, if you really wanted to, you could make Int conform to Error too.
The usual process would be this: the proposal author decides to revise their proposal, and the Language Steering Group decides whether the revised proposal requires further open review (in this case, it clearly would).
I suspect the LSG would discourage that revision, though. We generally donât want to pile different things up in a single proposal like that. Making Never behave like a true bottom type is conceptually complex and has a lot of implications that deserve to be given proper consideration, which they wouldnât be if we tacked it onto this proposal. Most of those implications have nothing to do with typed throws.
Agreed with John. "Make Never â„" is a (large!) evolution proposal on it's own, not something we would tack onto another already-substantial proposal.
I'm very much late for the review (only got this in my forum summary email just now), but after reading through the entire proposal, there's one decision that I don't fully understand:
With this proposal, plain throws is equivalent to throws(any Error), but throws(some Error) feels like a better fit to me. Since any Error: Error, this is strictly more expressive, and allows us to forgo existential boxing in certain cases. Specifically for the subtyping relationship between typed throws and plain throws, it seems like there has to be a hidden separate version of the typed-throwing function that boxes its error, which this could perhaps avoid? My knowledge is limited there, so I'll happily stand corrected if my intuition does not match the reality!
That's a distinct ABI, though. It's introducing a promise - that the function throws a specific error type - that wasn't there previously.
It's true that an opaque thrown type is likely more efficient than an existential, but because it limits the flexibility of the function (re. changes to its error behaviour in future) it's not something the compiler should impose on people.
It might be the case that a third option is best for a given case, too, such as throwing some CustomErrorProtocol to even further refine the API. But only the code author can decide that.
@Joe_Groff and I discussed some Error a bit here. Iâm not sure if we ever got a concrete answer to the ABI question, though, aside from Embedded Swift where thereâs an obvious difference because any Error doesnât exist.
Which seems like a pretty good reason for throws to mean throws(some Error), so that it behaves consistently on all targets. This hopefully shouldnât require an ABI change because Error is self-conforming.
This change could be introduced in Swift 6, given the other breaking changes planned for that release already, but I still think it's the wrong default. And I say that as someone who intends to use specific (not even opaque) error types broadly, contrary to the proposal's guidance of preferring any Error.
I do agree it could make sense to make some Error the default in "Embedded Swift". There's a lot about "Embedded"-mode Swift which is inevitably going to be different from "regular" Swift, so I don't think it's a big deal is this just another one of those things.
It doesn't make sense to restrict regular Swift to the strict subset of capabilities that Embedded Swift might be limited to. That would be very limiting and undermine Swift's benefits, in general use, over predecessor languages.
My hope is that throws(some Error) and throws(any Error) could be ABI-compatible so that clients building with existing toolchains would see an existential, but clients building with updated toolchains would see an opaque type (whose in-memory layout just happens to match that of an existential due to the existing special-casing of Error). That would enable throws to be an alias of throws(some Error) without restricting capabilities. Updated clients would default to locking the error into a single opaque type, but IMO thatâs a reasonable surface-level change to the language to introduce alongside typed errors.
Correct me if I'm wrong, but isn't the whole point of opaque types the flexibility to change them in the future while guaranteeing no source breaks (i.e. that clients weren't depending on the specific type it happened to be)? If a function currently happens to instantiate its plain throws' opaque type as CatError and later also starts throwing a KidError, its instantiation changes to any Error, which is still a concrete type conforming to some Error, just a different one.
Throwing an opaque error would require the caller to dynamically allocate space for the error before every call, whether the error is thrown or not. So no, itâs probably not more efficient.
(The space is allocated on the stack, but it still requires real work to set it up.)
Only insofar as they can change the specific type of the error, not that they can make it existential. Such as if the function were to start throwing either of two types of errors.
An existential type is a valid concrete type instantiating the opaque type, no? this is what I said in my last message. any Error is just as valid a replacement for some Error as CatErrorâonly for Error of course, since it's self-conforming.