[swift-evolution] Proposal: Typed throws

David Owens II david at owensd.io
Tue Dec 8 12:12:31 CST 2015

> On Dec 7, 2015, at 9:29 PM, Russ Bishop <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.

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.

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.

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

More information about the swift-evolution mailing list