I’m not saying we should! I’m saying that even if the language gets the ability to specify tight return types, it’s actually not a good idea to use them in many real-world cases.
To give a concrete example: for years the only storage on an iPhone was local NAND. If Foundation were being developed concurrently in Swift, one might advocate for giving NSData’s file reading errors a more specific type, such as FileSytemError, since that describes the sole category of errors that could occur on an early iPhone.
Then the iPad came along, along with the Camera Connection Kit. Suddenly iOS devices could have removable storage. You could try to retrofit this concept into FileSystemError by extending it to cover the newly-possible case of a filesystem completely disappearing in the middle of access.
And then iCloud file providers appear. Suddenly your filesystem access can fail for network-related reasons. Do we extend FileSystemError to now cover all possible underlying network-related errors, and if so, do we duplicate NetworkError’s API into FileSystemError, or do we give FileSystemError the ability to “wrap” a NetworkError? Are any of these changes even possible to make in an ABI-compatible way?
In complex systems, API surface methods have much more control over the values they return on success than they do over the reasons they can fail. Swift’s current design, privileging concrete return types and abstract error types, reflects this.
Erasing to Error doesn't provide the stability you seem to think it does. Unless the errors are themselves entirely opaque as well you're going to run into the same ABI and source stability issues that you would if you returned an error type directly. Otherwise how do you expect people to change behavior depending on the specifics of the error? Unless all you care about is the fact that an error occurred, which try? already handles, you're going to cast to specific error types and look at the specific case information.
In any case, Swift already handles the example you give by supporting frozen enums. Evolving the underlying error in that case is as simple as adding cases to the enum, as the language has already required the use @unknown default by the user. Of course it would be nice if this feature could be adopted by non stable libraries for things like errors, but the biggest issue, ABI stability, is already solved.
In short, this benefit you claim for erased errors doesn't actually exist.
Even if your service accepts random errors over the network and it makes sense for those errors to be exposed like other runtime errors, it's almost always more valuable to specifically type your errors and erase when it's beneficial than it is to erase and attempt to recover when you need it. You could, of course, contrive a service that accepts any random error of any type from any connection, but those are extremely rare (and probably not good services). Even in cases where the errors provided are "undocumented", like with URLSession, you can still exclusively handle the vast majority of cases with only a little work, leaving a single fallback handler for the random error types that can still crop up.
At the end of the day it would be really nice to combine the ergonomics of throws with the precision of typed errors. One should not have to use Result to maintain typing.
Okay, we don’t actually need to have exactly the same conversation we’ve had a dozen times before in the context of a short response about the interaction of this prospective feature with distributed actors.
So precise error typing is potentially blocking PATs for AsyncSequence, and I got a bit worried that AsyncSequence<Element> support might be delayed a lot until we see some progress in this thread?
I am not a compiler engineer or language engineer, but would it be possible for the compiler to make the error types implicit?
I would like the implicit annotation to extend to unions of errors too, not just throws to mean any Error - I imagine some developers may want to spell it out in the function signature, but some may prefer the ability to adapt to new error types without updating all signatures in their call stack.
For example:
func throwsA() throws {
throw A()
}
func throwsB() throws {
throw B()
}
func throwsAorB() throws {
if Bool.random() {
throw A()
} else {
throw B()
}
}
...
do {
try throwsA()
} catch { // error: A
}
...
do {
try throwsB()
} catch { // error: B
}
...
do {
try throwsAorB()
} catch let error as A { // error: A
} catch let error as B { // error: B
}