Nowadays, a throwing function is mean to throw anything that conforms to Error, which can make catch clauses unnecessarily complex.
This is a proposal for let functions marked as throws to specify which Error will be throwing, and this will be compiler type-safety backed up such:
enum AuthenticationError: Error {
case invalidCredentials
case unknownReason
}
func authenticate(with username: String, and password: String) throws AuthenticationError {
[...]
throw .invalidCredentials // can be inferred by the compiler due to the function signature.
throw NSError(domain: "my domain", code: -1, userInfo: nil) // error: NSError cannot be casted as an AuthenticationError
}
do {
try authenticate(with: "foo", and: "bar")
} catch {
print(error.localizedDescription) // error is a AuthenticationError
}
do {
try authenticate(with: "foo", and: "bar")
} catch let error as NSError { // error: AuthenticationError cannot be casted to NSError
// N/A
}
Maybe this is an old topic but seems interesting to revisit it and see how the type inference system is much stronger now and that error handling is somehow incomplete; in a language where the strong type system is one of its clutch, this topic seems very poorly treated and hard to follow when there's a heavy throwing reliant topic.
IIRC, it started as "there's no reason for typed errors", not a problem with type checker, and see if users need more (which is precisely this thread).
Personally, I still feel that it's a burden to annotate error types Java-style.
I have read the rationale, doesn't seem to cover this kind of cases. Also, I'd like to add that this is all backwards compatible so you, for example that you don't like this type of annotations can just use the current semantics without issues.
I've read all threads, from 2015 up to 2017. Swift has changed a lot in three years and I think that with Swift 3 on the future road, this could be a good moment too have a specific discussion on how would this be implemented, limits (does generics makes sense?), and congruent with the actual semantics.
It seems fair to raise this topic again in 2020 with a totally different Swift and a stronger community.
Also thank you for the recap of all of those threads, some of them inspired me to open this and go forward.
Thanks for the effort! I think it'd be helpful/informative if you summarise the threads, and note how things have changed. The latter of which is quite important.
A strong argument for introducing typed throws now is because Combine uses typed errors, and working with combine and non-typed throws is awkward and leads to annoying boilerplate. On my phone now, but can write som example code tomorrow.
For what itâs worth, I mostly stopped using throws when Result was added to the standard library, precisely because it selfâdocuments what can go wrong, and can be easily switched over to get the master list if the error is an enumeration.
The only time I still use throws is when I donât even have information about the possible error types myself, because the method only passes on failures from Foundation or some other library with untyped errors.
I agree that Result in several cases is better in today's world - but I don't see it playing well with the language at a native level once we finally get async/await by Swift 6.
It'd be great if the type system itself absolves the need for an actual type, for both the asynchronous and synchronous worlds.
Whenever I see things like this, showing the versioning troubles people are already having with errors, Iâm glad we donât have typed throws.
Specifically, things like this:
Using enumerations to represent Error s is inadvisable, as if new errors need to be introduced they cannot be added to existing enumerations. This leads to a proliferation of Error enumerations. "Fake" enumerations can be made using struct s and static let s, but these do not work with the nice Error pattern-match logic in catch blocks, requiring type casts.
At least they have the escape hatch of multiple enums! Itâs not pretty, and the better approach is to use a struct, but people use enums anyway
Could you imagine if we had this, and every library had been striving to include the error types of throwing functions as part of their signature? The ecosystem would be much more fragile.
I would consider the pitch I just linked a prerequisite for even considering typed throws. Otherwise itâs too dangerous to be made so easy. In the mean time, those who really want it have Result.
Additionally, as part of typed throws, I would consider making any enum which conforms to Error resilient by default (with an ability to opt-out by marking it @frozen). Thrown errors will be gaining ABI significance, so itâs important developers think about this.
I don't see any issue with what your statement tells. Is therre any issue with making a switch? Even further, that issue, if it is, it can occur now with the standard error catching but multiplied to infinity enums that conform to Error, so I rather stick with just one.
Can you describe how do you use Result when you have several result statements chained in a function? Do you unwrap all .value? what happens with the Errors? Are ignored? Doesn't feel like a safe approach but I'm open to have an example.
Just to recap what some people with strong knowledge of the language thinks about this:
There're strong opinions on the fact that typed throws are a thing to include into the language and that even it could be ABI backwards compatible using the introduced semantics. For record that someone has it here to read and have more context about this.
Also, I'd like to see more dos and don'ts with the proposal in code in order to see what issues the semantics would have so we can analyze edge cases and other maybe related topics.
All contributions and opinions are more than welcome!
Thank you.
Some topics to cover:
Generic throwing allowed
Protocol establishing Error type through associatedtype.
Would it make sense to introduce an â@unknown default:â counterpart to catch blocks. That is, if a library offers an Error enumeration that is not marked â@frozenâ the compiler will require a typed throw to add an â@unknown catchâ clause:
func throwingFoo() throws Bar { ... }
do {
try throwingFoo()
} catch Bar.baz {
// Handle the baz error
}
â Not all cases of âBarâ are handled
Add â@unknown catchâ clause
do {
try throwingFoo()
} catch {
// You can switch on error: Bar
}
or
do {
try throwingFoo()
} catch Bar.baz {
// Handle Bar.baz
} catch {
// You can switch on error, which has every case, even @unknown, except from Bar.baz because
// it was previously catch'ed.
}
The main good thing here is that just an empty catch will gather all errors regarding Bar's enum, and use a normal switch statement.
The issue is that the catch block throws an error with a potentially different dynamic type from the one thrown by f. It used to be that rethrows functions could not throw their own error at all, but it looks like that has since been fixed.
I would say the majority of topâlevel calling code that wonât pass on the error just hangs .get() onto the end of the operation to turn the whole thing back into a throwing expression. However, a minority of call sites do want to handle particular errors, and for those Iâve found the Result style to be valuable for several reasons:
As the library author, you donât have to separately document what can go wrong (or worry about forgetting to).
As a client developer, you donât have to search for documentation to find out how to spell what it is you are trying to catch; code completion and exhaustiveness fixâits will just tell you.
As the library author, you cannot accidentally forget to make the error type public (whereas throws will let you throw an internal type, which no one can catch).
Adding, changing or removing an error identifier is still a sourceâbreaking change of semantics, even if it is just string identifier or an NSErrorâs, error code that changed. In my opinion, you may as well let the API reflect it.
Admittedly, I would probably see the whole thing differently if you couldnât go back and forth between the two styles so easily. The only annoying thing I encounter sometimes is that .get() doesnât inherit @discardableResult, forcing you to use _ = more than I would like.