[swift-evolution] Proposal: Typed throws

John McCall rjmccall at apple.com
Tue Dec 8 12:57:49 CST 2015

> On Dec 8, 2015, at 10:12 AM, David Owens II via swift-evolution <swift-evolution at swift.org> wrote:
>> On Dec 7, 2015, at 9:29 PM, Russ Bishop <xenadu at gmail.com <mailto:xenadu at gmail.com>> wrote:
>> As a library author all this does is promote wrapping errors with useless wrappers. If I touch the filesystem, I am subject to any number of filesystem errors. If I touch the network I am subject to any number of network errors. What is gained by wrapping those? If the OS is updated and the system API throws a new type of error does that mean my library is broken? If not, then the supposed contract is a lie. If my library calls another library, I’ve multiplied the boilerplate to handle that error (now the user needs to catch OtherLibraryErrorType and MyLibraryErrorType where e.innerError is OtherLibraryErrorType). Even if you solved the fragile interface problem there’s still pressure to perform the wrapping (and pressure on library authors not to introduce new error types) because I upgraded the library and now all my catch clauses are incomplete, potentially causing massive breakage due to a minor change in some core library function that ripples outward.
> Ok, let’s take an example of what you’re describing; I’ll argue that I believe that what you are doing is leaking implementation details that should not be shared. I believe this to be the general case when you simply wrap errors to propagate them up the call stack. 
> func nameOfTheDayOfTheWeek() -> String { /* … */ }
> Now, let’s say you need to touch the filesystem to implement this because you have a list of days in a text file.
> There are several options here:
> Mark the func as throws so the IO errors can propagate out.
> Mark the func as throws so you can “wrap” the error.
> Mark the func as throws so you can return specific errors for your library.
> Return an optional value to signify the error.
> Assume the function always works because an invariant you state and convert the error to a “universal error” (as described in this doc: https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst <https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst>)
> The fact that the function is using IO is an implementation detail, and because of that, it’s my opinion that #1 and #2 are wrong as you’re just propagating the inner implementation details that are subject to change. Option #3 here really seems like overkill for the function as there are only two results: get the name or don’t get the name.
> So, this leaves us with #4 and #5.

For this example, #5 seems like the clearly correct answer.  Of course, this is mostly just evidence that artificial examples aren’t terribly helpful for this kind of discussion.

But in some ways, this is pretty key in the design.  As I covered in the rationale, failure modes — like most other kinds of complexity — explode very quickly in a way that’s pretty predictable by 0,1,∞: either there’s some intrinsic, mathematically-definable reason why the operation can only fail (recoverably) in one or two ways, or you’re inevitably going to slide into the maddening complexity of the world.  My concern is that any design that tries to address the former doesn’t encourage well-meaning programmers to waste their time trying to tame the second.

> Of all of the options I listed above, I think all of them have value except #2. However, I would limit the contexts of each of the types of errors because they all have something explicit to say about the contract they are creating. 
> If you intend for your users of your library to simply be catching you error, logging it, and moving on, what’s the point of using the throws construct in the first place? It would seem that you would be much better of simplifying your API and simply use the optional construct (this is the “Simple Domain Errors” construct).
> The question really comes down to the “specificity” section; the primary concern here is #2 from the list above - simply wrapping the errors. I think the danger of throwing this out citing Java as the example, is problematic. Java made many mistakes with how it handled exceptions, and Swift takes a different approach to what errors mean.

I agree that it’s important not to conflate all the mistakes that Java made here.  This is specifically about the wisdom of writing “throws X, Y”, which I think has been a clearly failed design approach in Java independent of all of those other mistakes.

> What I’m asking for in the proposal is the ability for library authors to chose to make a strong contract or a weak contract about the type of errors it can throw. Also, the other vital thing to note here is this: if I’m returning an enum that implements ErrorType, I’ve explicitly stated that these are the sum of the errors that I want to handle. Further, it only impacts the places in my code where I’ve explicitly chosen to handle the different cases of the error differently. If I were to instead specify a protocol or another type, the thing I’m getting out of that is the ability to use the APIs for those types without casting (that is also the exhaustive case, but suffers from none of the breaking change concerns you had above).
> It’s up to the library author to determine if their error types are a set of potential issues or a type that simply holds some information so that new error types can be added later. 
>>  If the OS is updated and the system API throws a new type of error does that mean my library is broken?
> Yes (if that error was defined as an enum), just like your libraries are broken if a library you use adds a new enum case; that’s a breaking change. You have the same problem today with this code:
> func f() -> MyEnum { /* … */ }
> let r = f()
> switch r {
>     case .First: /* do something */
>     case .Last: /* do something */
> }
> Do you also recommend always having a default case when working with enums throughout your code? This is just as fragile. 
> Also, if the counter-argument is that ErrorType is sufficient for all error handling, then Swift should just make it a struct and let’s be done with it.

This doesn’t follow; there’s still clear value in being able to define new library-specific errors.  The ErrorType design is essentially an open enumeration, akin to the SML exn type, with a considerable bit more structure due to the grouping into types (which can then implement more specific and reflectively-discoverable protocols).

Again, I’m not opposed to allowing more specificity here eventually.  I just think it’s not really a good use of design and engineering resources right now, because I think simple “throws” is going to be the right design for libraries far more often than some more specific type, especially given that in practice most error types will almost always be resilient enums and therefore will not allow exhaustive pattern-matching outside of their defining module anyway.

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151208/9481c558/attachment.html>

More information about the swift-evolution mailing list