Proposal: Typed throws

IMHO be careful what you wish for. If the compiler enforces this then we're
just repeating the mistakes of Java's checked exceptions. All roads would
eventually lead to "throws ErrorType", defeating the supposed purpose.

russ

Isn't it better to have the choice of type safety, and perhaps have a
compiler option or linter to enforce it (if you choose).

Default syntax:
func foo() throws; // defaults to ErrorType

Optional type safety:
func foo() throws(MyError); // note, only one type.

When it comes down to it, for me, the problem is that you can catch
anything you like, and the check for exhaustivity does not check what may
actually be thrown, resulting in excess code and compile-time errors.

···

On Tue, Dec 8, 2015 at 9:24 AM, Russ Bishop via swift-evolution < swift-evolution@swift.org> wrote:

IMHO be careful what you wish for. If the compiler enforces this then
we're just repeating the mistakes of Java's checked exceptions. All roads
would eventually lead to "throws ErrorType", defeating the supposed purpose.

russ

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Java mixes the ability to catch catastrophic errors with programming errors; that’s part of the problem. Swift’s approach is that the throws construct is for recoverable errors.

I’m perfectly fine with allowing:

func foo() throws -> () {}

And

func foo() throws MyError -> () {}

The proposal states that.

What the typed version allows is the ability specifically validate that all of the error constructs (assuming an enum ErrorType) have been handled. If you still want to use the “catch-all”, that’s fine, you can do so. I’m not proposing to take away your ability to throw any ErrorType conforming type you want to.

-David

···

On Dec 7, 2015, at 4:06 PM, Andrew Bennett via swift-evolution <swift-evolution@swift.org> wrote:

Isn't it better to have the choice of type safety, and perhaps have a compiler option or linter to enforce it (if you choose).

Default syntax:
func foo() throws; // defaults to ErrorType

Optional type safety:
func foo() throws(MyError); // note, only one type.

When it comes down to it, for me, the problem is that you can catch anything you like, and the check for exhaustivity does not check what may actually be thrown, resulting in excess code and compile-time errors.

On Tue, Dec 8, 2015 at 9:24 AM, Russ Bishop via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
IMHO be careful what you wish for. If the compiler enforces this then we're just repeating the mistakes of Java's checked exceptions. All roads would eventually lead to "throws ErrorType", defeating the supposed purpose.

russ

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

My $0.02:

When the errors thrown by a protocol are constrained to just MyError, what are your options for reporting other types of errors that your implementation can generate (such as a database error, IO error, and so on).

Is wrapping one of those in a MyError really that useful? Does that mean a MyError enumeration is going to contain an .Other(ErrorType) case to deal with them? Why not (in that case) just have the calling code catch said other exceptions and deal with them generically?

- Documenting the specific errors that a method may throw is useful.
- Indicating for some functions that *only* those specific types can be thrown is useful, and may warrant compiler support (enforcement of said declaration, allowing exhaustive catches)
- Documenting that you may also get exceptions dependent on objects or closures that you have supplied is also useful. Rethrows implicitly does a little of this today.

I however point to Java as an example that exhaustive checked exceptions are a bad idea (and in Java, they aren’t even exhaustive - RuntimeException and Error types and subtypes are unchecked). The use of Errors as a signaling mechanism for alternate flows should be kept to a minimum.

-DW

···

On Dec 7, 2015, at 5:06 PM, Andrew Bennett via swift-evolution <swift-evolution@swift.org> wrote:

Isn't it better to have the choice of type safety, and perhaps have a compiler option or linter to enforce it (if you choose).

Default syntax:
func foo() throws; // defaults to ErrorType

Optional type safety:
func foo() throws(MyError); // note, only one type.

When it comes down to it, for me, the problem is that you can catch anything you like, and the check for exhaustivity does not check what may actually be thrown, resulting in excess code and compile-time errors.

On Tue, Dec 8, 2015 at 9:24 AM, Russ Bishop via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
IMHO be careful what you wish for. If the compiler enforces this then we're just repeating the mistakes of Java's checked exceptions. All roads would eventually lead to "throws ErrorType", defeating the supposed purpose.

russ

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Both Java and C# have uncatchable (or un-stoppable) exceptions for various catastrophic errors and in fact Java’s checked exceptions are a lie partially for that reason. C# doesn’t have checked exceptions but go ahead and try to catch StackOverflowException or ThreadAbortException (the former will die immediately, the latter will pretend to be caught then get re-thrown).

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.

I understand the desire to avoid having the general catch-all but IMHO in most cases that is a mistake in design. If you aren’t prepared to handle any possible error and you aren’t rethrowing/throwing or retrying on all errors then you’re probably catching errors in the wrong place and should just pass them up the chain to someone else who can take action based on the error. Almost all error handling responses boil down to some combination of a) log it, b) try again later, or c) give up and die.

russ

···

On Dec 7, 2015, at 4:28 PM, David Owens II <david@owensd.io> wrote:

Java mixes the ability to catch catastrophic errors with programming errors; that’s part of the problem. Swift’s approach is that the throws construct is for recoverable errors.

I’m perfectly fine with allowing:

func foo() throws -> () {}

And

func foo() throws MyError -> () {}

The proposal states that.

What the typed version allows is the ability specifically validate that all of the error constructs (assuming an enum ErrorType) have been handled. If you still want to use the “catch-all”, that’s fine, you can do so. I’m not proposing to take away your ability to throw any ErrorType conforming type you want to.

-David

My personal use case for typed throws would have been as a replacement
(within the context of an interpreter for a different language) for:

func doSomething() -> Result<T, MyInterpreterRuntimeError> {
  guard someCondition() else {
    return .Failure(MyInterpreterRuntimeError(...))
  }
  guard anotherCondition() else {
    return .Failure(MyInterpreterRuntimeError(...))
  }
  // ...
  return .Success(T(...))
}

becomes...

func doSomething() (throws MyInterpreterRuntimeError) -> T {
  guard someCondition() else {
    throw MyInterpreterRuntimeError(...)
  }
  guard anotherCondition() else {
    throw MyInterpreterRuntimeError(...)
  }
  // ...
  return T(...
}

The more I think about this use case, though, the more I feel like I'm
misusing the mechanism for syntax sugar, as John alluded to earlier. If
this isn't a compelling argument for some sort of typed throws support,
though, I would at least like to see better support for a stdlib Result
type, although there are pitfalls there as well and that's a topic better
elaborated upon within a different thread.

Austin

···

On Mon, Dec 7, 2015 at 4:51 PM, David Waite via swift-evolution < swift-evolution@swift.org> wrote:

My $0.02:

When the errors thrown by a protocol are constrained to just MyError, what
are your options for reporting other types of errors that your
implementation can generate (such as a database error, IO error, and so
on).

Is wrapping one of those in a MyError really that useful? Does that mean a
MyError enumeration is going to contain an .Other(ErrorType) case to deal
with them? Why not (in that case) just have the calling code catch said
other exceptions and deal with them generically?

- Documenting the specific errors that a method may throw is useful.
- Indicating for some functions that *only* those specific types can be
thrown is useful, and may warrant compiler support (enforcement of said
declaration, allowing exhaustive catches)
- Documenting that you may also get exceptions dependent on objects or
closures that you have supplied is also useful. Rethrows implicitly does a
little of this today.

I however point to Java as an example that exhaustive checked exceptions
are a bad idea (and in Java, they aren’t even exhaustive - RuntimeException
and Error types and subtypes are unchecked). The use of Errors as a
signaling mechanism for alternate flows should be kept to a minimum.

-DW

On Dec 7, 2015, at 5:06 PM, Andrew Bennett via swift-evolution < > swift-evolution@swift.org> wrote:

Isn't it better to have the choice of type safety, and perhaps have a
compiler option or linter to enforce it (if you choose).

Default syntax:
func foo() throws; // defaults to ErrorType

Optional type safety:
func foo() throws(MyError); // note, only one type.

When it comes down to it, for me, the problem is that you can catch
anything you like, and the check for exhaustivity does not check what may
actually be thrown, resulting in excess code and compile-time errors.

On Tue, Dec 8, 2015 at 9:24 AM, Russ Bishop via swift-evolution < > swift-evolution@swift.org> wrote:

IMHO be careful what you wish for. If the compiler enforces this then
we're just repeating the mistakes of Java's checked exceptions. All roads
would eventually lead to "throws ErrorType", defeating the supposed purpose.

russ

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

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\)

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.

-David

···

On Dec 7, 2015, at 9:29 PM, Russ Bishop <xenadu@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\)

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.

John.

···

On Dec 8, 2015, at 10:12 AM, David Owens II via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 7, 2015, at 9:29 PM, Russ Bishop <xenadu@gmail.com <mailto:xenadu@gmail.com>> wrote:

Then let's change my response to simply: :+1:

Thanks!

···

On Tue, Dec 8, 2015 at 11:28 AM, David Owens II <david@owensd.io> wrote:

Java mixes the ability to catch catastrophic errors with programming
errors; that’s part of the problem. Swift’s approach is that the throws
construct is for *recoverable* errors.

I’m perfectly fine with allowing:

func foo() throws -> () {}

And

func foo() throws MyError -> () {}

The proposal states that.

What the typed version allows is the ability specifically validate that
all of the error constructs (assuming an enum ErrorType) have been handled.
If you still want to use the “catch-all”, that’s fine, you can do so. I’m
not proposing to take away your ability to throw any ErrorType conforming
type you want to.

-David

On Dec 7, 2015, at 4:06 PM, Andrew Bennett via swift-evolution < > swift-evolution@swift.org> wrote:

Isn't it better to have the choice of type safety, and perhaps have a
compiler option or linter to enforce it (if you choose).

Default syntax:
func foo() throws; // defaults to ErrorType

Optional type safety:
func foo() throws(MyError); // note, only one type.

When it comes down to it, for me, the problem is that you can catch
anything you like, and the check for exhaustivity does not check what may
actually be thrown, resulting in excess code and compile-time errors.

On Tue, Dec 8, 2015 at 9:24 AM, Russ Bishop via swift-evolution < > swift-evolution@swift.org> wrote:

IMHO be careful what you wish for. If the compiler enforces this then
we're just repeating the mistakes of Java's checked exceptions. All roads
would eventually lead to "throws ErrorType", defeating the supposed purpose.

russ

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Huge +1 to David's comments. The language should not restrict the choices available to library authors when designing the contract for error results.

···

Sent from my iPad

On Dec 8, 2015, at 12:12 PM, David Owens II via swift-evolution <swift-evolution@swift.org> wrote:

On Dec 7, 2015, at 9:29 PM, Russ Bishop <xenadu@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\)

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.

-David

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution