Going through some posts that I noted for reading later. This looks like a hard case for source compatibility.
If we can't infer { throw SomeError } to be of type () throws SomeError -> () then we have a problem, because inference should always infer the most special type possible? At least in this case.
Now we can redirect the discussion to the pitch thread here: Typed throws all feedback from this thread has been analyzed and recorded into the proposal in order to go forward with a formal proposal.
You did bot forgot a case. I was talking about slightly different thing. You may not be in control of a protocol or an exception definition for the multiple reasons.
What i find acceptable is if throws Would allowed to list the possible exceptions for some compile time warnings/checks. But not limit the thrower to use only them.
Could you give an example of the kinds of compile-time checks you would get from such a system? It seems like this wouldnât be much better than un-typed throws + documentation.
Which case? Even if you force the user of a protocol to use your specific error that you force with throws SpecificError then why can't an implementer of this protocol catch all errors and convert them to the specific error you force on them.
@torstenlehmann
Sorry, I missed your reply. So, being a java developer for 8 years I saw a lot of situation then the protocol requires you to throw some particular error. And it is good that this error type allows you at least to propagate underlying error. It was quite often that it is not. We may talk alot about what is good or bad API but this is what I remember from my practice. I can be wrong. Last time I touched Java it was Java 6 and EJB. But, how I would like to see that is: if we are going to bring typed exceptions, it should not be strict and compiler would suggest you to catch a default error as well.
Basically something like this: func doSomething() throws TypedError
The code that uses that function should be
do {
try doSomething()
catch error: TypedError {
}
catch error { // And forgetting this part should be a compiler error
}
What I hated the most is that:
func doSomoethingElse() throws SomeOtherTypedError {
try doSomething() // Compile error: TypedError does not match SomeOtherTypedError
}
And you spend half of the time rewrapping errors and 90% of the times you dont even care what exactly happened there. You just need to know that this method did not work as expected.
But in the end if you don't respect what the semantics are describing they don't make sense at all. You couldn't allow that code to be correct because you're breaking the deal between the throw types that are explicitly described in the language doesn't it?
If you donât care what was thrown you can always use try? to collapse the return value into a simple âworkedâ or âdidnât workâ category. I admit we donât have a good solution to this for functions that are purely for side-effects with no return value. Perhaps there is a way to improve that.
I agree with your point for the general case. Most of the time if something underneath you can fail, it can fail in new and exciting ways that are impossible to fully enumerate ahead of time. Things can go wrong in more ways than they can go right. Strong and fully-enumerated types are great for reasoning about the well-behavior of systems, but can make reasoning about the mis-behavior of systems harder. We really want to avoid error mappings, continuously churning taxonomies, and especially things like a case unknown(Error) which suffers the worst of both worlds.
That being said, typed errors would be really really nice for libraries whose operations can fail in well-defined and limited ways. For System, which is both a binary stable and source stable library, we held the party line that throws is the Swifty expression of failure and we'd have to find some workaround for the error boxing issues. Here's the workaround we found:
Our ABI is expressed in terms of Result for performance but our API is expressed in terms of throws. That is, emit the Result -> throws mapping into user code hoping that the optimizer can learn to unbox the Int32 (Errno here is just a wrapper around In32). We have @_alwaysEmitIntoClient instead of @inlinable because we want to eject the name close from our ABI in case Swift ever does get typed throws: removing or warning on an unneeded catch statement is more feasible than breaking binary compatibility.
Outside of throwing Errnos, there are still several uses for typed throws. They're useful for higher-fidelity alternatives to things like failable initializers and preconditions.
For example, inside the standard library we have String initializers that decode UTF-8 and either fix up encoding errors as they go, or else return nil if there are errors. We don't have a good way to communicate why a decode went wrong from an initializer. We could add a static method that returns a Result<String, UTF8DecodingError>, but typed throws on an initializer would be Swiftier. We don't want untyped throws on the init, because the fact that failure produces a DecodingErroris part of the API's contract and we would consider throwing anything else to be a hard break of both source and binary compatibility. Untyped throws gives the users a false impression that something else might be thrown in the future and it gives the library maintainer the false impression that they could throw something else in the future. It also makes the catch sites ugly.
Over in the part of System that does not wrap POSIX calls, we have another (related) use case for typed throws. FilePath.Component can be created from a String literal but contains the precondition that the string is not empty nor does it contain a directory separator (/), i.e. it is exactly one component. It also has a failable initializer that will just returns nil. It would be nice to have something that did both, a way to fail that carries the precondition message up the stack with it. Here we want to throw a concrete error, and we expect it to commonly be automatically propagated upwards as an untyped error.
In System, throws currently carries a connotation that the underlying system is consulted (since syscalls can fail). If we had types on throws, it would be explicit in the code and checked by the compiler. This would then open us up to using error handling for other errors generated within library code itself.
This is very well-reasoned and quite convincing. It would be lovely to have a way to address this use case.
While we donât (and shouldnât) reject designs simply because they can be misused or used suboptimally, what the numerous discussions about typed throws here have shown is that many users are motivated to use typed throws precisely in the general case, where the motivating considerations you put forward wouldnât apply but the consequences youâve outlined would apply.
I wonder if there is some solution which could satisfy the use cases you present here without being immediately an attractive nuisance in the general case. Can we look to the situation we have for frozen enums for inspiration, such that a typed throw is a guarantee of exhaustiveness and more of an advanced-level feature for ABI stable and source stable libraries?
I think deciding what kind of error handling a programmer should use without being familiar with their code base is a bit presumptuous. Not everyone is writing a module that needs to evolve errors over time.
My takeaway from this discussion has been that many people here, myself included, are in fact dealing with closed systems with finite enumerable errors and are underwhelmed with how this interacts with throw.
That is true. But speaking from personal experience with Java:
You tend to underestimate how ground may shift under you as technology and requirements evolve and corner you with surfacing new failure modes being imposed from a (possibly newly added) lower layer.
Well, I guess the only experience I have here is adding a lower layer dependency with our current untyped throws system. This was a terrible experience, because the Lower level API (Foundation.FileManager) did not specify what kinds of errors it throws. There was no way to handle its errors properly, and no way for me to even create tests for this because thereâs no way to know what can go wrong. I canât even tell the user what went wrong, all I can say is âsomething failed when trying to access a fileâ and abort the entire file-related operation.
This is a perfect example of an API author deciding that calling code (my code) didnât need to know what went wrong, when it would have been much better if they had surfaced as much information as possible and allow me to choose what, if any, I want to ignore. What if it was a permissions error, or a disk full error? This is actionable information that could help the user solve the problem. Instead weâll never know.
Several people up-thread are correct that I would have decided to handle a couple of specific cases here and then sweep everything else into a generic catch bucket, but with untyped errors I never got the opportunity to pick out those specific cases.
I don't understand. Apple frameworks return error information in NSError objects, which bridge into Swift Errors, from which you can retrieve the error domain and error code that tell you what the error means. This is explained in documentation, along with an example of analyzing FileManager errors.
FileManager, like most Apple frameworks based on NSError reporting, doesn't come under the category of closed systems with finite enumerable errors, so bridging them over to Swift as (hypothetical) typed throws wouldn't actually be helpful.
That documentation and example isnât linked from the FileManager API I was using, so IMO it just goes to show that documentation is really not sufficient and this would be better if it was enforced by the compiler. Indeed, having read the documentation the problem remains: I still donât know what errors can come from these functions. Yes they are bridged from NSError but what domain(s)? What codes? Is it only the domain in this example? Questions abound.
NSError is not a closed system, but file operations surely are. In an(my?) ideal world these functions would be typed throws. Because they are not typed I have to either:
Have a single untyped catch clause with some kind of generic response
Guess what the errors might be and handle those, resulting probably in catch cases that will never be used (Isnât this what people were saying would happen with typed throws?)
The type information would still be present in the function signature in some form, so I'm not sure how anything else would be different from func foo(...) throws DecodingError -> T. I don't know if uglifyingt it would actually help. Do you have something in mind?
I do think an emphasis on leaf libraries, with exhaustive errors and stable interfaces, can help guide the feature set. For example, it's either throws or throwing a single type. An anonymous sum of errors would not be supported: either typed throws is not the solution you want or the thrown type is important enough to deserve a name and doc comment.
Not to go too deep into the weeds here, but the set of basic file operation errors has actually changed a decent amount over the years on Darwin platforms due to things like iCloud fault files, TCC, full disk encryption, filesystem compression, etc⌠I've even very rarely seen syscalls produce undocumented errno values.
These are, admittedly, pretty edge-case scenarios, so please don't read this as expressing a strong opinion on the topic at hand. Just anecdata that might be interesting.
I don't have a full design in mind, more a general direction to explore. I'm not speaking to the color of the bikeshed specifically here, rather the functionality itself: For instance, can we make only a resilient library's declaration of a typed throw permit 'exhaustive catching,' while all other uses still require the user to have some sort of default error handling.