Proposal: Allow Type Annotations on Throws


(David Owens II) #1

This a significantly updated proposal for typed annotations on the `throws` construct. The previous was closed due to not be complete; I believe I’ve addressed all of those concerns.

https://github.com/owensd/swift-evolution/blob/master/proposals/allow-type-annotations-on-throw.md

···

Allow Type Annotation on Throws
Proposal: SE-NNNN <applewebdata://9C8E792C-9803-4E70-811E-D5169EDD5F6D>
Author(s): David Owens II <applewebdata://9C8E792C-9803-4E70-811E-D5169EDD5F6D>
Status: Pending Approval for Review
Review manager: TBD
Introduction
The error handling system within Swift today creates an implicitly loose contract on the API. While this can be desirable in some cases, it’s certainly not desired in all cases. This proposal looks at modifying how the error handling mechanism works today by adding the ability to provide a strong API contract.

Error Handling State of the Union
This document will use the terminology and the premises defined in the Error Handling Rationale <https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst> document.

To very briefly summarize, there are four basic classification of errors:

Simple Domain Errors
Recoverable Errors
Universal Errors
Logic Failures
Each of these types of errors are handled differently at the call sites. Today, only the first two are directly handled by Swift error handling mechanism. The second two are uncatchable in Swift (such as fatalError(), ObjC exceptions, and force-unwrapping of null optionals).

Simple Domain Errors

As stated in Error Handling Rationale <https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst> document, the “Swift way” to handle such errors is to return an Optional<T>.

func parseInt(value: String) -> Int? {}
The simple fact of the result being Optional.None signifies that the string could not be parsed and converted into an Int. No other information is necessary or warranted.

Recoverable Errors

In this context, these are errors that need to provide additional information to the caller. The caller can then decide a course of action that needs to be taken. This could be any number of things, including, but not limited to, logging error information, attempting a retry, or potentially invoking a different code path. All of these errors implement the ErrorType protocol.

func openFile(filename: String) throws {}
The throws keyword annotates that the function can return additional error information. The caller must also explicitly make use of this when invoking the function.

do {
  try openFile("path/to/somewhere")
}
catch {}
Errors are able to propagate if called within another context that can throw, thus alleviating the annoying “catch and rethrow” behavior:

func parent() throws {
  try openFile("path/to/somwhere")
}
Lastly, functions can be marked to selectively throw errors if they take a function parameter that throws with the rethrows keyword. The really interesting part is that it’s only necessary to use try when calling the function with a throwing closure.

func openFile(filename: String) throws {}
func say(message: String) {}

func sample(fn: (_: String) throws -> ()) rethrows {
    try fn("hi")
}

try sample(openFile)
sample(say)
Converting Recoverable Errors to Domain Errors

Swift also has the try? construct. The notable thing about this construct is that it allows the caller to turn a “Recoverable Error” into a “Simple Domain Error”.

if let result = try? openFile("") {}
ErrorType Implementors

Errors are implemented using the ErrorType protocol. Since it is a protocol, new error types can be a class, a struct, or an enum. A type qualified throws clause would allow code authors to change the way that the catch-clauses need to be structured.

Enum Based ErrorType

When enums are used as the throwing mechanism, a generic catch-clause is still required as the compiler doesn’t have enough information. This leads to ambiguous code paths.

enum Errors: ErrorType {
    case OffBy1
    case MutatedValue
}

func f() throws { throw Errors.OffBy1 }

do {
    try f()
}
catch Errors.OffBy1 { print("increment by 1") }
catch Errors.MutatedValue { fatalError("data corrupted") }
The above code requires a catch {} clause, but it’s ambiguous what that case should do. There is no right way to handle this error. If the error is ignored, we’re now in the land of “Logic Errors”; the code path should never be hit. If we use a fatalError() construct, then we are now in the land of converting a potential compiler error into a “Universal Error”.

Both of these are undesirable.

Struct and Class Based ErrorType

In the current design, errors that are thrown require a catch-all all the time. In the proposed design, which will be explained further, a catch-all would not be required if there was a case-clause that matched the base type.

class ErrorOne: ErrorType {}
func g() throws { throw ErrorOne() }

do {
    try g()
}
catch is ErrorOne { print("ErrorOne") }
The advantage in these cases are different, these cases do not allow pattern matching over the error type members (as you can in a switch-statement, for example).

The workaround for this functionality is this:

class ErrorOne: ErrorType {
    let value: Int
    init(_ value: Int) { self.value = value }
}

do {
    try g()
}
catch {
    if let e = error as? ErrorOne {
        switch e {
        case _ where e.value == 0: print("0")
        case _ where e.value == 1: print("1")
        default: print("nothing")
        }
    }
}
This proposal would turn the above into:

class ErrorOne: ErrorType {
    let value: Int
    init(_ value: Int) { self.value = value }
}

do {
    try g()
}
catch _ where error.value == 0 { print("0") }
catch _ where error.value == 1 { print("1") }
catch { print("nothing") }
}
No gymnastics to go through, just straight-forward pattern-matching like you’d expect.

NOTE: This requires the promotion of the error constant to be allowed through the entirety of the catch-clauses.

Overriding

In the context of types, it’s completely possible to override functions with the throws annotations. The rules simply follow the rules today: covariance on the return type is allowed, contravariance is not.

Generics

When looking at generics, I cannot come up with a reason why they shouldn’t just work as normal:

func gen<SomeError: ErrorType>() throws SomeError {}
The only constraint would be that the specified error type must adhere to the ErrorType protocol. However, this is no different than today:

func f<T>(a: T) throws { throw a }
This results in the compiler error:

Thrown expression type ’T’ does not conform to ‘ErrorType’
This seems like it should “just work”.

Design Change Proposal
The design change is simple and straight-forward: allow for the annotation of the type of error that is being returned as an optional restriction. The default value would still be ErrorType.

func specific() throws MyError {}
func nonspecific() throws {}
There is a secondary result of this proposal: the error constant should be promoted to be allowed for use through-out all of the catch-clauses.

Impact on Existing Code

This is a non-breaking change. All existing constructs work today without change. That said, there are a few places where this change will have an impact on future usage.

Function Declarations

When a function has a throws clause that is attributed with a type, then that type becomes part of the function signature. This means that these two functions are not considered to be of the same type:

func one() throws {}
func two() throws NumberError {}
The function signatures are covariant though, so either one or two can be assigned to f below:

let f: () throws -> ()
This is completely fine as NumberError still implements the ErrorType protocol.

However, in this case:

let g: () throws NumberError -> ()
It would not be valid to assign one to g as the type signature is more specific.

throws and rethrows

Functions currently have the ability to be marked as rethrows. This basically says that if a closure parameter can throw, then the function will throw too.

func whatever(fn: () throws -> ()) rethrows {}
The whatever function is up for anything that fn is up for. Keeping in line with this mentality, the rethrows would exhibit the same behavior: typed annotations simply apply if present and do not if they are missing.

func specific(fn: () throws HappyError -> ()) rethrows {}
This all works as expected:

func f() throws HappyError {}
func g() {}

try specific(f)
specific(g)
This works for the same covariant reason as the non-qualified throws implementation works: a non-throwing function is always able to be passed in for a throwing function.

The do-catch statement

There are two rule changes here, but again, it’s non-breaking.

The first rule change is to promote the error constant that would normally only be allowed in the catch-all clause (no patterns) to be available throughout each of the catch clauses. This allows for the error information to be used in pattern matching, which is especially valuable in the non-enum case.

The second change is to allow the error constant to take on a specific type when all of the throwing functions throw the same specified type. When this is the case, two things become possible:

In the enum-type implementation of ErrorType, the catch-clauses can now be exhaustive.
In the all of the cases, the API of the specific ErrorType becomes available in the catch-clause without casting the error constant. This greatly simplifies the pattern-matching process.
In the case that there are heterogenous ErrorType implementations being returned, the errorconstant simply has the type of ErrorType.

The try call sites

There is no change for the try, try?, or try! uses. The only clarification I’ll add is that try?is still the appropriate way to promote an error from a “Recoverable Error” to a “Simple Domain Error”.

Alternate Proposals
There is another common error handling mechanism used in the community today: Either<L, R>. There are various implementations, but they all basically boil down to an enum that captures the value or the error information.

I actually consider my proposal syntactic sugar over this concept. If and when Swift supports covariant generics, there is not a significant reason I can see why the underlying implementation could not just be that.

The advantage is that the proposed (and existing) syntax of throws greatly increases the readability and understanding that this function actually possesses the ability to throw errors and they should be handled.

The other advantage of this syntax is that it doesn’t require a new construct to force the usage of the return type.

Further, if functions where to ever gain the ability to be marked as async, this could now be handled naturally within the compiler as the return type could a promise-like implementation for those.

Criticisms
From the earlier threads on the swift-evolution mailing list, there are a few primary points of contention about this proposal.

Aren’t we just creating Java checked-exceptions, which we all know are terrible?

No. The primary reason is that a function can only return a single error-type. The other major reason is that the error philosophy is very different in Swift than in Java.

Aren’t we creating fragile APIs that can cause breaking changes?

Potentially, yes. This depends on how the ABI is handled in Swift 3 for enums. The same problem exists today, although at a lesser extent, for any API that returns an enum today.

Chris Lattner mentioned this on the thread:

The resilience model addresses how the public API from a module can evolve without breaking clients (either at the source level or ABI level). Notably, we want the ability to be able to add enum cases to something by default, but also to allow API authors to opt into more performance/strictness by saying that a public enum is “fragile” or “closed for evolution”.
So if enums have an attribute that allows API authors to denote the fragility enums, then this can be handled via that route.

Another potential fix is that only internal and private scoped functions are allowed to use the exhaustive-style catch-clauses. For all public APIs, they would still need the catch-all clauses.

For APIs that return non-enum based ErrorType implementations, then no, this does not contribute to the fragility problem.

Aren’t we creating the need for wrapper errors?

This is a philosophical debate. I’ll simply state that I believe that simply re-throwing an error, say some type of IO error, from your API that is not an IO-based API is design flaw: you are exposing implementation details to users. This creates a fragile API surface.

Also, since the type annotation is opt-in, I feel like this is a really minor argument. If your function is really able to throw errors from various different API calls, then just stick with the default ErrorType.


Why doesn't Swift have explicit throwables like Java
(Matthew Johnson) #2

David,

Thank you for taking the time to continue working on a proposal for typed throws. I agree that this feature is very desirable and appreciate the work you’re doing to bring forward a proposal. I think it’s a great start but also has some room for improvement.

First, I think it could be strengthened by incorporating some learning from Rust. My impression is that the Rust community is very happy with typed error handling. Adding some detail about their experience would provide a counter-example to those who are concerned about the experience in Java and C++.

I agree that error types are an important part of an API contract. One of the big hurdles to doing this well is the need to catch errors when all that needs to be done is to wrap and rethrow them. Ideally should not need to do this just to perform a simple type translation to map the underlying error into the type we wish to expose as part of a stable API contract. You might want to take a look at the From mechanism Rust uses to facilitate this. IMO a proposal for typed error handling should address this issue in some way (even if the author determines this mechanism is not necessary or a good design cannot be identified).

I would also like to see much more detail on why you think allowing a function to throw multiple error types is problematic. My impression is that you have concerns from a usability point of view. I am on the fence here to some degree, but definitely leaning in the direction that allowing a function to throw multiple error types is better.

The primary reason I lean this way is that it enables more re-use of standard error types. Custom error types for an API often make sense, but not always. I am concerned about the need to create them just because our API contract might reasonably include two or three of the standard error types. Adding new types when they are not necessary introduces complexity and cognitive overhead. It also complicates catching of errors if the new custom type is a two or three case enum that just embeds the underlying error.

These problems will lead many people to just revert to an untyped throws clause. Objections to typed errors along these lines are common and legitimate. They will arise during review. It is best if you address them in the proposal now in order to focus a review on your solutions. My personal opinion is that allowing multiple error types and including a mechanism to perform automatic wrapping when appropriate would go a long way towards solving them.

Implementation challenges related to multi-typed errors have been discussed on the list quite a bit already. They would obviously need to be addressed if we go in that direction. I don’t want to downplay those. But I do think we need to try to identify the most usable solution for typed errors that we can first and then focus on implementation details. If the design needs to be modified to accommodate implementation at least we will have a better idea of what we are giving up.

I am willing to be convinced that a single error type is better than multiple error types but the current proposal does not provide a compelling argument in that direction. It just says “Java checked exceptions”. I know these have been pretty much universally considered a serious design mistake. My impression is that there are quite a few reasons for that. I don’t have any direct experience with Java and am not familiar with the details. If you could elaborate on specifically why you believe allowing multiple error types was a significant contributor to the problem in a manner that indicates that they will be a problem in any language that includes them I would appreciate that. Links would be sufficient if they are focused on answering this particular question.

I’m looking forward to your feedback on these thoughts.

Thanks,
Matthew

···

On Dec 18, 2015, at 1:29 AM, David Owens II via swift-evolution <swift-evolution@swift.org> wrote:

This a significantly updated proposal for typed annotations on the `throws` construct. The previous was closed due to not be complete; I believe I’ve addressed all of those concerns.

https://github.com/owensd/swift-evolution/blob/master/proposals/allow-type-annotations-on-throw.md

Allow Type Annotation on Throws
Proposal: SE-NNNN <applewebdata://E64F564F-E0C7-443D-BB19-243FAD102941>
Author(s): David Owens II <applewebdata://E64F564F-E0C7-443D-BB19-243FAD102941>
Status: Pending Approval for Review
Review manager: TBD
Introduction
The error handling system within Swift today creates an implicitly loose contract on the API. While this can be desirable in some cases, it’s certainly not desired in all cases. This proposal looks at modifying how the error handling mechanism works today by adding the ability to provide a strong API contract.

Error Handling State of the Union
This document will use the terminology and the premises defined in the Error Handling Rationale <https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst> document.

To very briefly summarize, there are four basic classification of errors:

Simple Domain Errors
Recoverable Errors
Universal Errors
Logic Failures
Each of these types of errors are handled differently at the call sites. Today, only the first two are directly handled by Swift error handling mechanism. The second two are uncatchable in Swift (such as fatalError(), ObjC exceptions, and force-unwrapping of null optionals).

Simple Domain Errors

As stated in Error Handling Rationale <https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst> document, the “Swift way” to handle such errors is to return an Optional<T>.

func parseInt(value: String) -> Int? {}
The simple fact of the result being Optional.None signifies that the string could not be parsed and converted into an Int. No other information is necessary or warranted.

Recoverable Errors

In this context, these are errors that need to provide additional information to the caller. The caller can then decide a course of action that needs to be taken. This could be any number of things, including, but not limited to, logging error information, attempting a retry, or potentially invoking a different code path. All of these errors implement the ErrorType protocol.

func openFile(filename: String) throws {}
The throws keyword annotates that the function can return additional error information. The caller must also explicitly make use of this when invoking the function.

do {
  try openFile("path/to/somewhere")
}
catch {}
Errors are able to propagate if called within another context that can throw, thus alleviating the annoying “catch and rethrow” behavior:

func parent() throws {
  try openFile("path/to/somwhere")
}
Lastly, functions can be marked to selectively throw errors if they take a function parameter that throws with the rethrows keyword. The really interesting part is that it’s only necessary to use try when calling the function with a throwing closure.

func openFile(filename: String) throws {}
func say(message: String) {}

func sample(fn: (_: String) throws -> ()) rethrows {
    try fn("hi")
}

try sample(openFile)
sample(say)
Converting Recoverable Errors to Domain Errors

Swift also has the try? construct. The notable thing about this construct is that it allows the caller to turn a “Recoverable Error” into a “Simple Domain Error”.

if let result = try? openFile("") {}
ErrorType Implementors

Errors are implemented using the ErrorType protocol. Since it is a protocol, new error types can be a class, a struct, or an enum. A type qualified throws clause would allow code authors to change the way that the catch-clauses need to be structured.

Enum Based ErrorType

When enums are used as the throwing mechanism, a generic catch-clause is still required as the compiler doesn’t have enough information. This leads to ambiguous code paths.

enum Errors: ErrorType {
    case OffBy1
    case MutatedValue
}

func f() throws { throw Errors.OffBy1 }

do {
    try f()
}
catch Errors.OffBy1 { print("increment by 1") }
catch Errors.MutatedValue { fatalError("data corrupted") }
The above code requires a catch {} clause, but it’s ambiguous what that case should do. There is no right way to handle this error. If the error is ignored, we’re now in the land of “Logic Errors”; the code path should never be hit. If we use a fatalError() construct, then we are now in the land of converting a potential compiler error into a “Universal Error”.

Both of these are undesirable.

Struct and Class Based ErrorType

In the current design, errors that are thrown require a catch-all all the time. In the proposed design, which will be explained further, a catch-all would not be required if there was a case-clause that matched the base type.

class ErrorOne: ErrorType {}
func g() throws { throw ErrorOne() }

do {
    try g()
}
catch is ErrorOne { print("ErrorOne") }
The advantage in these cases are different, these cases do not allow pattern matching over the error type members (as you can in a switch-statement, for example).

The workaround for this functionality is this:

class ErrorOne: ErrorType {
    let value: Int
    init(_ value: Int) { self.value = value }
}

do {
    try g()
}
catch {
    if let e = error as? ErrorOne {
        switch e {
        case _ where e.value == 0: print("0")
        case _ where e.value == 1: print("1")
        default: print("nothing")
        }
    }
}
This proposal would turn the above into:

class ErrorOne: ErrorType {
    let value: Int
    init(_ value: Int) { self.value = value }
}

do {
    try g()
}
catch _ where error.value == 0 { print("0") }
catch _ where error.value == 1 { print("1") }
catch { print("nothing") }
}
No gymnastics to go through, just straight-forward pattern-matching like you’d expect.

NOTE: This requires the promotion of the error constant to be allowed through the entirety of the catch-clauses.

Overriding

In the context of types, it’s completely possible to override functions with the throws annotations. The rules simply follow the rules today: covariance on the return type is allowed, contravariance is not.

Generics

When looking at generics, I cannot come up with a reason why they shouldn’t just work as normal:

func gen<SomeError: ErrorType>() throws SomeError {}
The only constraint would be that the specified error type must adhere to the ErrorType protocol. However, this is no different than today:

func f<T>(a: T) throws { throw a }
This results in the compiler error:

Thrown expression type ’T’ does not conform to ‘ErrorType’
This seems like it should “just work”.

Design Change Proposal
The design change is simple and straight-forward: allow for the annotation of the type of error that is being returned as an optional restriction. The default value would still be ErrorType.

func specific() throws MyError {}
func nonspecific() throws {}
There is a secondary result of this proposal: the error constant should be promoted to be allowed for use through-out all of the catch-clauses.

Impact on Existing Code

This is a non-breaking change. All existing constructs work today without change. That said, there are a few places where this change will have an impact on future usage.

Function Declarations

When a function has a throws clause that is attributed with a type, then that type becomes part of the function signature. This means that these two functions are not considered to be of the same type:

func one() throws {}
func two() throws NumberError {}
The function signatures are covariant though, so either one or two can be assigned to f below:

let f: () throws -> ()
This is completely fine as NumberError still implements the ErrorType protocol.

However, in this case:

let g: () throws NumberError -> ()
It would not be valid to assign one to g as the type signature is more specific.

throws and rethrows

Functions currently have the ability to be marked as rethrows. This basically says that if a closure parameter can throw, then the function will throw too.

func whatever(fn: () throws -> ()) rethrows {}
The whatever function is up for anything that fn is up for. Keeping in line with this mentality, the rethrows would exhibit the same behavior: typed annotations simply apply if present and do not if they are missing.

func specific(fn: () throws HappyError -> ()) rethrows {}
This all works as expected:

func f() throws HappyError {}
func g() {}

try specific(f)
specific(g)
This works for the same covariant reason as the non-qualified throws implementation works: a non-throwing function is always able to be passed in for a throwing function.

The do-catch statement

There are two rule changes here, but again, it’s non-breaking.

The first rule change is to promote the error constant that would normally only be allowed in the catch-all clause (no patterns) to be available throughout each of the catch clauses. This allows for the error information to be used in pattern matching, which is especially valuable in the non-enum case.

The second change is to allow the error constant to take on a specific type when all of the throwing functions throw the same specified type. When this is the case, two things become possible:

In the enum-type implementation of ErrorType, the catch-clauses can now be exhaustive.
In the all of the cases, the API of the specific ErrorType becomes available in the catch-clause without casting the error constant. This greatly simplifies the pattern-matching process.
In the case that there are heterogenous ErrorType implementations being returned, the errorconstant simply has the type of ErrorType.

The try call sites

There is no change for the try, try?, or try! uses. The only clarification I’ll add is that try?is still the appropriate way to promote an error from a “Recoverable Error” to a “Simple Domain Error”.

Alternate Proposals
There is another common error handling mechanism used in the community today: Either<L, R>. There are various implementations, but they all basically boil down to an enum that captures the value or the error information.

I actually consider my proposal syntactic sugar over this concept. If and when Swift supports covariant generics, there is not a significant reason I can see why the underlying implementation could not just be that.

The advantage is that the proposed (and existing) syntax of throws greatly increases the readability and understanding that this function actually possesses the ability to throw errors and they should be handled.

The other advantage of this syntax is that it doesn’t require a new construct to force the usage of the return type.

Further, if functions where to ever gain the ability to be marked as async, this could now be handled naturally within the compiler as the return type could a promise-like implementation for those.

Criticisms
From the earlier threads on the swift-evolution mailing list, there are a few primary points of contention about this proposal.

Aren’t we just creating Java checked-exceptions, which we all know are terrible?

No. The primary reason is that a function can only return a single error-type. The other major reason is that the error philosophy is very different in Swift than in Java.

Aren’t we creating fragile APIs that can cause breaking changes?

Potentially, yes. This depends on how the ABI is handled in Swift 3 for enums. The same problem exists today, although at a lesser extent, for any API that returns an enum today.

Chris Lattner mentioned this on the thread:

The resilience model addresses how the public API from a module can evolve without breaking clients (either at the source level or ABI level). Notably, we want the ability to be able to add enum cases to something by default, but also to allow API authors to opt into more performance/strictness by saying that a public enum is “fragile” or “closed for evolution”.
So if enums have an attribute that allows API authors to denote the fragility enums, then this can be handled via that route.

Another potential fix is that only internal and private scoped functions are allowed to use the exhaustive-style catch-clauses. For all public APIs, they would still need the catch-all clauses.

For APIs that return non-enum based ErrorType implementations, then no, this does not contribute to the fragility problem.

Aren’t we creating the need for wrapper errors?

This is a philosophical debate. I’ll simply state that I believe that simply re-throwing an error, say some type of IO error, from your API that is not an IO-based API is design flaw: you are exposing implementation details to users. This creates a fragile API surface.

Also, since the type annotation is opt-in, I feel like this is a really minor argument. If your function is really able to throw errors from various different API calls, then just stick with the default ErrorType.

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


(Chris Lattner) #3

I haven’t had a chance to read this update in detail (but I have skimmed it): with that as a caveat, I am s till +1 on the proposal, and agree that we should stick with 0 or 1 declared thrown types. It should also only happen after resilience features for enums are in place.

-Chris

···

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

This a significantly updated proposal for typed annotations on the `throws` construct. The previous was closed due to not be complete; I believe I’ve addressed all of those concerns.

https://github.com/owensd/swift-evolution/blob/master/proposals/allow-type-annotations-on-throw.md


(David Owens II) #4

David,

Thank you for taking the time to continue working on a proposal for typed throws. I agree that this feature is very desirable and appreciate the work you’re doing to bring forward a proposal. I think it’s a great start but also has some room for improvement.

First, I think it could be strengthened by incorporating some learning from Rust. My impression is that the Rust community is very happy with typed error handling. Adding some detail about their experience would provide a counter-example to those who are concerned about the experience in Java and C++.

I’m not involved in the Rust community so I wouldn’t feel comfortable making claims for them.

I agree that error types are an important part of an API contract. One of the big hurdles to doing this well is the need to catch errors when all that needs to be done is to wrap and rethrow them. Ideally should not need to do this just to perform a simple type translation to map the underlying error into the type we wish to expose as part of a stable API contract. You might want to take a look at the From mechanism Rust uses to facilitate this. IMO a proposal for typed error handling should address this issue in some way (even if the author determines this mechanism is not necessary or a good design cannot be identified).

The From() construct seems like a map(T) -> U problem, but it seems heavily tied into the ability to create sum types. Swift doesn’t have this feature, and that feature is out-of-scope for this proposal. More on this later.

I would also like to see much more detail on why you think allowing a function to throw multiple error types is problematic. My impression is that you have concerns from a usability point of view. I am on the fence here to some degree, but definitely leaning in the direction that allowing a function to throw multiple error types is better.

Sure. There’s no functionality today to auto-generate a sum type in Swift today, and that is what this request really is. If you want to return multiple return types, then you need to do exactly what Rust does and create a sum type that composes the various types of errors. This exposes the same potential fragile API surface as extending enums do. I did call this part out specifically.

I see this functionality as a general limitation in the language. For example, errors are not the only context where you may want to return a type of A, B, or C. There have been other proposals on how we might do that in Swift. If and when it was solved in the general case for type parameters, I can’t foresee a compelling reason why it wouldn’t work in this context as well.

I am willing to be convinced that a single error type is better than multiple error types but the current proposal does not provide a compelling argument in that direction. It just says “Java checked exceptions”. I know these have been pretty much universally considered a serious design mistake. My impression is that there are quite a few reasons for that. I don’t have any direct experience with Java and am not familiar with the details. If you could elaborate on specifically why you believe allowing multiple error types was a significant contributor to the problem in a manner that indicates that they will be a problem in any language that includes them I would appreciate that. Links would be sufficient if they are focused on answering this particular question.

I guess I should have just specifically called this out in the proposal. It’s not because of “Java checked exceptions”, it’s because nowhere else in the language are types allowed to be essentially annotated in a sum-like fashion. We can’t directly say a function returns an Int or a String. We can’t say a parameter can take an Int or a Double. Similarly, I propose we can’t say a function can return an error A or B.

Thus, the primary reason is about type-system consistency.

Swift already supports a construct to create sum types: associated enums. What it doesn’t allow is the ability to create them in a syntactic shorthand. In this way, my error proposal does the same thing as Rust: multiple return types need to be combined into a single-type - enum.

On to Félix’s comments now!

-David

···

On Dec 18, 2015, at 5:53 AM, Matthew Johnson <matthew@anandabits.com> wrote:


(Félix Cloutier) #5

The biggest problem I have with error handling is that most schemes aren't predictable. I like that Swift's model forces you to reduce the possible errors to a single type, most easily an enum type, because this promotes predictability: your function can fail for no more or no less than what the enum has. This goes hand in hand with forcing try on functions that throw, so that you always know what could go wrong and what has gone wrong.

What I invariably hate about the common exception model and the common error code model is that the error conditions are either theoretically or practically infinite. It makes it hard to determine which errors you can actually trigger, which in turns makes it hard to decide which cases should be handled and which cases should be forwarded, which perpetuates that it's hard to determine which errors you can trigger.

Integral error codes are limited, but the limit is far above what human beings can model and consider, and each integral value can be overloaded. Microsoft's HRESULT and Apple's OSStatus are two examples of large and overloaded status codes (see osstatus.com for how bad this can get). Luckily, this model is discouraged with Swift.

Polymorphic exceptions, on their end, have two major shortfalls: they allow an infinite class of errors to be raised at any point where the call graph isn't entirely static (since developers can subclass the error class), and they encourage errors to be classified by kind instead of by cause.

In my opinion, kind-based exception hierarchies are the original sin of exception handling. For a spectacular failure of exception design, look no further than Java's IOException hierarchy. A method that catches an IOException may be dealing with a ClosedChannelException, a MalformedURLException, a UserPrincipalNotFoundException, an UnsupportedEncodingException, or any other of the 31 direct known subclasses (or a subclass of any of these), or any other user subclass. These are all things that can happen during some I/O, but it makes IOException as a catchable type entirely meaningless.

Unfortunately, it's extremely tempting to categorize exceptions by kind, because object-oriented programming is all about categorizing things by kind. This was just one of the platoon of exception hierarchies that fell down the proverbial slippery slope.

For this reason, I don't like to encourage throwing polymorphic types, and I think that it's a misconception to pretend that having a single type in the throws annotation ensures that the function throws a single type. In my opinion, the fact that it's currently possible but awkward to use polymorphic types as errors is exactly as much support as the feature should receive.

···

Le 18 déc. 2015 à 08:53:44, Matthew Johnson via swift-evolution <swift-evolution@swift.org> a écrit :

David,

Thank you for taking the time to continue working on a proposal for typed throws. I agree that this feature is very desirable and appreciate the work you’re doing to bring forward a proposal. I think it’s a great start but also has some room for improvement.

First, I think it could be strengthened by incorporating some learning from Rust. My impression is that the Rust community is very happy with typed error handling. Adding some detail about their experience would provide a counter-example to those who are concerned about the experience in Java and C++.

I agree that error types are an important part of an API contract. One of the big hurdles to doing this well is the need to catch errors when all that needs to be done is to wrap and rethrow them. Ideally should not need to do this just to perform a simple type translation to map the underlying error into the type we wish to expose as part of a stable API contract. You might want to take a look at the From mechanism Rust uses to facilitate this. IMO a proposal for typed error handling should address this issue in some way (even if the author determines this mechanism is not necessary or a good design cannot be identified).

I would also like to see much more detail on why you think allowing a function to throw multiple error types is problematic. My impression is that you have concerns from a usability point of view. I am on the fence here to some degree, but definitely leaning in the direction that allowing a function to throw multiple error types is better.

The primary reason I lean this way is that it enables more re-use of standard error types. Custom error types for an API often make sense, but not always. I am concerned about the need to create them just because our API contract might reasonably include two or three of the standard error types. Adding new types when they are not necessary introduces complexity and cognitive overhead. It also complicates catching of errors if the new custom type is a two or three case enum that just embeds the underlying error.

These problems will lead many people to just revert to an untyped throws clause. Objections to typed errors along these lines are common and legitimate. They will arise during review. It is best if you address them in the proposal now in order to focus a review on your solutions. My personal opinion is that allowing multiple error types and including a mechanism to perform automatic wrapping when appropriate would go a long way towards solving them.

Implementation challenges related to multi-typed errors have been discussed on the list quite a bit already. They would obviously need to be addressed if we go in that direction. I don’t want to downplay those. But I do think we need to try to identify the most usable solution for typed errors that we can first and then focus on implementation details. If the design needs to be modified to accommodate implementation at least we will have a better idea of what we are giving up.

I am willing to be convinced that a single error type is better than multiple error types but the current proposal does not provide a compelling argument in that direction. It just says “Java checked exceptions”. I know these have been pretty much universally considered a serious design mistake. My impression is that there are quite a few reasons for that. I don’t have any direct experience with Java and am not familiar with the details. If you could elaborate on specifically why you believe allowing multiple error types was a significant contributor to the problem in a manner that indicates that they will be a problem in any language that includes them I would appreciate that. Links would be sufficient if they are focused on answering this particular question.

I’m looking forward to your feedback on these thoughts.

Thanks,
Matthew

On Dec 18, 2015, at 1:29 AM, David Owens II via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

This a significantly updated proposal for typed annotations on the `throws` construct. The previous was closed due to not be complete; I believe I’ve addressed all of those concerns.

https://github.com/owensd/swift-evolution/blob/master/proposals/allow-type-annotations-on-throw.md


(Dennis Lysenko) #6

Genuinely, David, thank you for taking up the mantle of this problem. To
me, the lack of type annotations makes the error handling painful and I
wish the team just hadn't released it until the design was smoothed out
fully. Those dangling catch-all blocks when I've caught all cases make it
uselessly verbose and moreover do not fit with the language at all.

As for the multiple vs. single type annotations, I do think that you need a
concrete example of why this will be different from java to quell the
concerns of all the people that skim the proposal, don't think about it,
and exclaim "but everyone hates it in Java!" so I agree on the singular
type annotations.

Also, your point about being able to mark functions async strongly supports
single type annotations until we get union types (if ever).

I am glad you addressed covariance/contravariance and the semantics of
function types when they throw general errors vs. a specific one.

One question, mainly to the compiler team: would it be reasonable to be
able to use generics covariant over the throws operator? For example, I
could define a function that takes a function which throws E and returns R,
and creates a function that takes a callback which takes an argument of
type Either<R, E> instead. This would be an incredibly powerful feature.

···

On Fri, Dec 18, 2015, 8:53 AM Matthew Johnson via swift-evolution < swift-evolution@swift.org> wrote:

David,

Thank you for taking the time to continue working on a proposal for typed
throws. I agree that this feature is very desirable and appreciate the
work you’re doing to bring forward a proposal. I think it’s a great start
but also has some room for improvement.

First, I think it could be strengthened by incorporating some learning
from Rust. My impression is that the Rust community is very happy with
typed error handling. Adding some detail about their experience would
provide a counter-example to those who are concerned about the experience
in Java and C++.

I agree that error types are an important part of an API contract. One of
the big hurdles to doing this well is the need to catch errors when all
that needs to be done is to wrap and rethrow them. Ideally should not need
to do this just to perform a simple type translation to map the underlying
error into the type we wish to expose as part of a stable API contract.
You might want to take a look at the From mechanism Rust uses to facilitate
this. IMO a proposal for typed error handling should address this issue in
some way (even if the author determines this mechanism is not necessary or
a good design cannot be identified).

I would also like to see much more detail on why you think allowing a
function to throw multiple error types is problematic. My impression is
that you have concerns from a usability point of view. I am on the fence
here to some degree, but definitely leaning in the direction that allowing
a function to throw multiple error types is better.

The primary reason I lean this way is that it enables more re-use of
standard error types. Custom error types for an API often make sense, but
not always. I am concerned about the need to create them just because our
API contract might reasonably include two or three of the standard error
types. Adding new types when they are not necessary introduces complexity
and cognitive overhead. It also complicates catching of errors if the new
custom type is a two or three case enum that just embeds the underlying
error.

These problems will lead many people to just revert to an untyped throws
clause. Objections to typed errors along these lines are common and
legitimate. They will arise during review. It is best if you address them
in the proposal now in order to focus a review on your solutions. My
personal opinion is that allowing multiple error types and including a
mechanism to perform automatic wrapping when appropriate would go a long
way towards solving them.

Implementation challenges related to multi-typed errors have been
discussed on the list quite a bit already. They would obviously need to be
addressed if we go in that direction. I don’t want to downplay those. But
I do think we need to try to identify the most usable solution for typed
errors that we can first and then focus on implementation details. If the
design needs to be modified to accommodate implementation at least we will
have a better idea of what we are giving up.

I am willing to be convinced that a single error type is better than
multiple error types but the current proposal does not provide a compelling
argument in that direction. It just says “Java checked exceptions”. I
know these have been pretty much universally considered a serious design
mistake. My impression is that there are quite a few reasons for that. I
don’t have any direct experience with Java and am not familiar with the
details. If you could elaborate on specifically why you believe allowing
multiple error types was a significant contributor to the problem in a
manner that indicates that they will be a problem in any language that
includes them I would appreciate that. Links would be sufficient if they
are focused on answering this particular question.

I’m looking forward to your feedback on these thoughts.

Thanks,
Matthew

On Dec 18, 2015, at 1:29 AM, David Owens II via swift-evolution < > swift-evolution@swift.org> wrote:

This a significantly updated proposal for typed annotations on the
`throws` construct. The previous was closed due to not be complete; I
believe I’ve addressed all of those concerns.

https://github.com/owensd/swift-evolution/blob/master/proposals/allow-type-annotations-on-throw.md

Allow Type Annotation on Throws

   - Proposal: SE-NNNN
   - Author(s): David Owens II
   - Status: *Pending Approval for Review*
   - Review manager: TBD

Introduction

The error handling system within Swift today creates an implicitly loose
contract on the API. While this can be desirable in some cases, it’s
certainly not desired in *all* cases. This proposal looks at modifying
how the error handling mechanism works today by adding the ability to
provide a strong API contract.
Error Handling State of the Union

This document will use the terminology and the premises defined in the Error
Handling Rationale
<https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst>
document.

To very briefly summarize, there are four basic classification of errors:

   1. Simple Domain Errors
   2. Recoverable Errors
   3. Universal Errors
   4. Logic Failures

Each of these types of errors are handled differently at the call sites.
Today, only the first two are directly handled by Swift error handling
mechanism. The second two are uncatchable in Swift (such as fatalError(),
ObjC exceptions, and force-unwrapping of null optionals).
Simple Domain Errors

As stated in Error Handling Rationale
<https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst> document,
the “Swift way” to handle such errors is to return an Optional<T>.

func parseInt(value: String) -> Int? {}

The simple fact of the result being Optional.None signifies that the
string could not be parsed and converted into an Int. No other
information is necessary or warranted.
Recoverable Errors

In this context, these are errors that need to provide additional
information to the caller. The caller can then decide a course of action
that needs to be taken. This could be any number of things, including, but
not limited to, logging error information, attempting a retry, or
potentially invoking a different code path. All of these errors implement
the ErrorType protocol.

func openFile(filename: String) throws {}

The throws keyword annotates that the function can return additional
error information. The caller must also explicitly make use of this when
invoking the function.

do {
  try openFile("path/to/somewhere")
}
catch {}

Errors are able to propagate if called within another context that can
throw, thus alleviating the annoying “catch and rethrow” behavior:

func parent() throws {
  try openFile("path/to/somwhere")
}

Lastly, functions can be marked to selectively throw errors if they take a
function parameter that throws with the rethrows keyword. The really
interesting part is that it’s only necessary to use try when calling the
function with a throwing closure.

func openFile(filename: String) throws {}
func say(message: String) {}

func sample(fn: (_: String) throws -> ()) rethrows {
    try fn("hi")
}

try sample(openFile)
sample(say)

Converting Recoverable Errors to Domain Errors

Swift also has the try? construct. The notable thing about this construct
is that it allows the caller to turn a “Recoverable Error” into a “Simple
Domain Error”.

if let result = try? openFile("") {}

ErrorType Implementors

Errors are implemented using the ErrorType protocol. Since it is a
protocol, new error types can be a class, a struct, or an enum. A type
qualified throws clause would allow code authors to change the way that
the catch-clauses need to be structured.
Enum Based ErrorType

When enums are used as the throwing mechanism, a generic catch-clause is
still required as the compiler doesn’t have enough information. This leads
to ambiguous code paths.

enum Errors: ErrorType {
    case OffBy1
    case MutatedValue
}

func f() throws { throw Errors.OffBy1 }

do {
    try f()
}
catch Errors.OffBy1 { print("increment by 1") }
catch Errors.MutatedValue { fatalError("data corrupted") }

The above code requires a catch {} clause, but it’s ambiguous what that
case should do. There is no *right* way to handle this error. If the
error is ignored, we’re now in the land of “Logic Errors”; the code path
should never be hit. If we use a fatalError() construct, then we are now
in the land of converting a potential compiler error into a “Universal
Error”.

Both of these are undesirable.
Struct and Class Based ErrorType

In the current design, errors that are thrown require a catch-all all the
time. In the proposed design, which will be explained further, a catch-all
would not be required if there was a case-clause that matched the base type.

class ErrorOne: ErrorType {}
func g() throws { throw ErrorOne() }

do {
    try g()
}
catch is ErrorOne { print("ErrorOne") }

The advantage in these cases are different, these cases do not allow
pattern matching over the error type members (as you can in a
switch-statement, for example).

The workaround for this functionality is this:

class ErrorOne: ErrorType {
    let value: Int
    init(_ value: Int) { self.value = value }
}

do {
    try g()
}
catch {
    if let e = error as? ErrorOne {
        switch e {
        case _ where e.value == 0: print("0")
        case _ where e.value == 1: print("1")
        default: print("nothing")
        }
    }
}

This proposal would turn the above into:

class ErrorOne: ErrorType {
    let value: Int
    init(_ value: Int) { self.value = value }
}

do {
    try g()
}
catch _ where error.value == 0 { print("0") }
catch _ where error.value == 1 { print("1") }
catch { print("nothing") }
}

No gymnastics to go through, just straight-forward pattern-matching like
you’d expect.

NOTE: This requires the promotion of the error constant to be allowed
through the entirety of the catch-clauses.
Overriding

In the context of types, it’s completely possible to override functions
with the throws annotations. The rules simply follow the rules today:
covariance on the return type is allowed, contravariance is not.
Generics

When looking at generics, I cannot come up with a reason why they
shouldn’t just work as normal:

func gen<SomeError: ErrorType>() throws SomeError {}

The only constraint would be that the specified error type must adhere to
the ErrorType protocol. However, this is no different than today:

func f<T>(a: T) throws { throw a }

This results in the compiler error:

Thrown expression type ’T’ does not conform to ‘ErrorType’

This seems like it should “just work”.
Design Change Proposal

The design change is simple and straight-forward: allow for the annotation
of the type of error that is being returned as an optional restriction. The
default value would still be ErrorType.

func specific() throws MyError {}
func nonspecific() throws {}

There is a secondary result of this proposal: the error constant should
be promoted to be allowed for use through-out all of the catch-clauses.
Impact on Existing Code

This is a non-breaking change. All existing constructs work today without
change. That said, there are a few places where this change will have an
impact on future usage.
Function Declarations

When a function has a throws clause that is attributed with a type, then
that type becomes part of the function signature. This means that these two
functions are not considered to be of the same type:

func one() throws {}
func two() throws NumberError {}

The function signatures are covariant though, so either one or two can be
assigned to f below:

let f: () throws -> ()

This is completely fine as NumberError still implements the ErrorType
protocol.

However, in this case:

let g: () throws NumberError -> ()

It would not be valid to assign one to g as the type signature is more
specific.
throws and rethrows

Functions currently have the ability to be marked as rethrows. This
basically says that if a closure parameter can throw, then the function
will throw too.

func whatever(fn: () throws -> ()) rethrows {}

The whatever function is up for anything that fn is up for. Keeping in
line with this mentality, the rethrows would exhibit the same behavior:
typed annotations simply apply if present and do not if they are missing.

func specific(fn: () throws HappyError -> ()) rethrows {}

This all works as expected:

func f() throws HappyError {}
func g() {}

try specific(f)
specific(g)

This works for the same covariant reason as the non-qualified throws implementation
works: a non-throwing function is always able to be passed in for a
throwing function.
The do-catch statement

There are two rule changes here, but again, it’s non-breaking.

The first rule change is to promote the error constant that would
normally only be allowed in the catch-all clause (no patterns) to be
available throughout each of the catch clauses. This allows for the error
information to be used in pattern matching, which is especially valuable in
the non-enum case.

The second change is to allow the error constant to take on a specific
type when *all* of the throwing functions throw the same specified type.
When this is the case, two things become possible:

   1. In the enum-type implementation of ErrorType, the catch-clauses can
   now be exhaustive.
   2. In the all of the cases, the API of the specific ErrorType becomes
   available in the catch-clause without casting the error constant. This
   greatly simplifies the pattern-matching process.

In the case that there are heterogenous ErrorType implementations being
returned, the errorconstant simply has the type of ErrorType.
The try call sites

There is no change for the try, try?, or try! uses. The only
clarification I’ll add is that try?is still the appropriate way to
promote an error from a “Recoverable Error” to a “Simple Domain Error”.
Alternate Proposals

There is another common error handling mechanism used in the community
today: Either<L, R>. There are various implementations, but they all
basically boil down to an enum that captures the value or the error
information.

I actually consider my proposal syntactic sugar over this concept. If and
when Swift supports covariant generics, there is not a significant reason I
can see why the underlying implementation could not just be that.

The advantage is that the proposed (and existing) syntax of throws greatly
increases the readability and understanding that this function actually
possesses the ability to throw errors and they should be handled.

The other advantage of this syntax is that it doesn’t require a new
construct to force the usage of the return type.

Further, if functions where to ever gain the ability to be marked as async,
this could now be handled naturally within the compiler as the return type
could a promise-like implementation for those.
Criticisms

From the earlier threads on the swift-evolution mailing list, there are a
few primary points of contention about this proposal.
Aren’t we just creating Java checked-exceptions, which we all know are
terrible?

No. The primary reason is that a function can only return a single
error-type. The other major reason is that the error philosophy is very
different in Swift than in Java.
Aren’t we creating fragile APIs that can cause breaking changes?

Potentially, yes. This depends on how the ABI is handled in Swift 3 for
enums. The same problem exists today, although at a lesser extent, for any
API that returns an enum today.

Chris Lattner mentioned this on the thread:

The resilience model addresses how the public API from a module can evolve
without breaking clients (either at the source level or ABI level).
Notably, we want the ability to be able to add enum cases to something by
default, but also to allow API authors to opt into more
performance/strictness by saying that a public enum is “fragile” or “closed
for evolution”.

So if enums have an attribute that allows API authors to denote the
fragility enums, then this can be handled via that route.

Another potential fix is that *only* internal and private scoped
functions are allowed to use the exhaustive-style catch-clauses. For all
public APIs, they would still need the catch-all clauses.

For APIs that return non-enum based ErrorType implementations, then no,
this does not contribute to the fragility problem.
Aren’t we creating the need for wrapper errors?

This is a philosophical debate. I’ll simply state that I believe that
simply re-throwing an error, say some type of IO error, from your API that
is not an IO-based API is design flaw: you are exposing implementation
details to users. This creates a fragile API surface.

Also, since the type annotation is opt-in, I feel like this is a really
minor argument. If your function is really able to throw errors from
various different API calls, then just stick with the default ErrorType.
_______________________________________________
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


(David Owens II) #7

I don’t follow this. Declaring a type that is an enum or a struct absolutely guarantees that the function only returns a single type. If the type is a class-based error, then sure, there’s not guarantee.

However, the only option today is polymorphic error types.

-David

···

On Dec 18, 2015, at 7:03 AM, Félix Cloutier <felixcca@yahoo.ca> wrote:

For this reason, I don't like to encourage throwing polymorphic types, and I think that it's a misconception to pretend that having a single type in the throws annotation ensures that the function throws a single type. In my opinion, the fact that it's currently possible but awkward to use polymorphic types as errors is exactly as much support as the feature should receive.


(Matthew Johnson) #8

The biggest problem I have with error handling is that most schemes aren't predictable. I like that Swift's model forces you to reduce the possible errors to a single type, most easily an enum type, because this promotes predictability: your function can fail for no more or no less than what the enum has. This goes hand in hand with forcing try on functions that throw, so that you always know what could go wrong and what has gone wrong.

What I invariably hate about the common exception model and the common error code model is that the error conditions are either theoretically or practically infinite. It makes it hard to determine which errors you can actually trigger, which in turns makes it hard to decide which cases should be handled and which cases should be forwarded, which perpetuates that it's hard to determine which errors you can trigger.

Integral error codes are limited, but the limit is far above what human beings can model and consider, and each integral value can be overloaded. Microsoft's HRESULT and Apple's OSStatus are two examples of large and overloaded status codes (see osstatus.com <http://osstatus.com/> for how bad this can get). Luckily, this model is discouraged with Swift.

+1 to these comments. A finite list of non-polymorphic error types (ideally enums to explicitly declare the cases) has the same advantages of discoverability. It also has the potential advantage that it is would be easier to re-use error types. If this is done carefully it could facilitate improved re-use of error handling code. The potential downside is that it could encourage less thoughtful API contracts where people just add any error type they might trigger to the list. An error translation mechanism similar to From in Rust is probably necessary to mitigate this.

Polymorphic exceptions, on their end, have two major shortfalls: they allow an infinite class of errors to be raised at any point where the call graph isn't entirely static (since developers can subclass the error class), and they encourage errors to be classified by kind instead of by cause.

In my opinion, kind-based exception hierarchies are the original sin of exception handling. For a spectacular failure of exception design, look no further than Java's IOException hierarchy. A method that catches an IOException may be dealing with a ClosedChannelException, a MalformedURLException, a UserPrincipalNotFoundException, an UnsupportedEncodingException, or any other of the 31 direct known subclasses (or a subclass of any of these), or any other user subclass. These are all things that can happen during some I/O, but it makes IOException as a catchable type entirely meaningless.

Agree. This sounds awful.

Unfortunately, it's extremely tempting to categorize exceptions by kind, because object-oriented programming is all about categorizing things by kind. This was just one of the platoon of exception hierarchies that fell down the proverbial slippery slope.

For this reason, I don't like to encourage throwing polymorphic types, and I think that it's a misconception to pretend that having a single type in the throws annotation ensures that the function throws a single type. In my opinion, the fact that it's currently possible but awkward to use polymorphic types as errors is exactly as much support as the feature should receive.

I agree with most of what you’re stating here. Except that the only type information currently available for throwing functions is that they throw `ErrorType`. It doesn’t get more polymorphic than that!

···

On Dec 18, 2015, at 9:03 AM, Félix Cloutier <felixcca@yahoo.ca> wrote:

Le 18 déc. 2015 à 08:53:44, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> a écrit :

David,

Thank you for taking the time to continue working on a proposal for typed throws. I agree that this feature is very desirable and appreciate the work you’re doing to bring forward a proposal. I think it’s a great start but also has some room for improvement.

First, I think it could be strengthened by incorporating some learning from Rust. My impression is that the Rust community is very happy with typed error handling. Adding some detail about their experience would provide a counter-example to those who are concerned about the experience in Java and C++.

I agree that error types are an important part of an API contract. One of the big hurdles to doing this well is the need to catch errors when all that needs to be done is to wrap and rethrow them. Ideally should not need to do this just to perform a simple type translation to map the underlying error into the type we wish to expose as part of a stable API contract. You might want to take a look at the From mechanism Rust uses to facilitate this. IMO a proposal for typed error handling should address this issue in some way (even if the author determines this mechanism is not necessary or a good design cannot be identified).

I would also like to see much more detail on why you think allowing a function to throw multiple error types is problematic. My impression is that you have concerns from a usability point of view. I am on the fence here to some degree, but definitely leaning in the direction that allowing a function to throw multiple error types is better.

The primary reason I lean this way is that it enables more re-use of standard error types. Custom error types for an API often make sense, but not always. I am concerned about the need to create them just because our API contract might reasonably include two or three of the standard error types. Adding new types when they are not necessary introduces complexity and cognitive overhead. It also complicates catching of errors if the new custom type is a two or three case enum that just embeds the underlying error.

These problems will lead many people to just revert to an untyped throws clause. Objections to typed errors along these lines are common and legitimate. They will arise during review. It is best if you address them in the proposal now in order to focus a review on your solutions. My personal opinion is that allowing multiple error types and including a mechanism to perform automatic wrapping when appropriate would go a long way towards solving them.

Implementation challenges related to multi-typed errors have been discussed on the list quite a bit already. They would obviously need to be addressed if we go in that direction. I don’t want to downplay those. But I do think we need to try to identify the most usable solution for typed errors that we can first and then focus on implementation details. If the design needs to be modified to accommodate implementation at least we will have a better idea of what we are giving up.

I am willing to be convinced that a single error type is better than multiple error types but the current proposal does not provide a compelling argument in that direction. It just says “Java checked exceptions”. I know these have been pretty much universally considered a serious design mistake. My impression is that there are quite a few reasons for that. I don’t have any direct experience with Java and am not familiar with the details. If you could elaborate on specifically why you believe allowing multiple error types was a significant contributor to the problem in a manner that indicates that they will be a problem in any language that includes them I would appreciate that. Links would be sufficient if they are focused on answering this particular question.

I’m looking forward to your feedback on these thoughts.

Thanks,
Matthew

On Dec 18, 2015, at 1:29 AM, David Owens II via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

This a significantly updated proposal for typed annotations on the `throws` construct. The previous was closed due to not be complete; I believe I’ve addressed all of those concerns.

https://github.com/owensd/swift-evolution/blob/master/proposals/allow-type-annotations-on-throw.md


(Matthew Johnson) #9

Genuinely, David, thank you for taking up the mantle of this problem. To me, the lack of type annotations makes the error handling painful and I wish the team just hadn't released it until the design was smoothed out fully. Those dangling catch-all blocks when I've caught all cases make it uselessly verbose and moreover do not fit with the language at all.

As for the multiple vs. single type annotations, I do think that you need a concrete example of why this will be different from java to quell the concerns of all the people that skim the proposal, don't think about it, and exclaim "but everyone hates it in Java!" so I agree on the singular type annotations.

I definitely agree that we need to address the Java problem. That said, I don’t think avoiding knee-jerk objection from people who don’t read and consider a proposal carefully is a good way to approach design. There may be good reasons to choose singular type annotations but this is not one of them.

I believe Félix raised good points about polymorphism and complex hierarchy causing problems in Java. That sounds like it is the source of at least a significant part of the problems with Java’s checked exception model.

···

On Dec 18, 2015, at 9:25 AM, Dennis Lysenko <dennis.s.lysenko@gmail.com> wrote:
Also, your point about being able to mark functions async strongly supports single type annotations until we get union types (if ever).

I am glad you addressed covariance/contravariance and the semantics of function types when they throw general errors vs. a specific one.

One question, mainly to the compiler team: would it be reasonable to be able to use generics covariant over the throws operator? For example, I could define a function that takes a function which throws E and returns R, and creates a function that takes a callback which takes an argument of type Either<R, E> instead. This would be an incredibly powerful feature.

On Fri, Dec 18, 2015, 8:53 AM Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
David,

Thank you for taking the time to continue working on a proposal for typed throws. I agree that this feature is very desirable and appreciate the work you’re doing to bring forward a proposal. I think it’s a great start but also has some room for improvement.

First, I think it could be strengthened by incorporating some learning from Rust. My impression is that the Rust community is very happy with typed error handling. Adding some detail about their experience would provide a counter-example to those who are concerned about the experience in Java and C++.

I agree that error types are an important part of an API contract. One of the big hurdles to doing this well is the need to catch errors when all that needs to be done is to wrap and rethrow them. Ideally should not need to do this just to perform a simple type translation to map the underlying error into the type we wish to expose as part of a stable API contract. You might want to take a look at the From mechanism Rust uses to facilitate this. IMO a proposal for typed error handling should address this issue in some way (even if the author determines this mechanism is not necessary or a good design cannot be identified).

I would also like to see much more detail on why you think allowing a function to throw multiple error types is problematic. My impression is that you have concerns from a usability point of view. I am on the fence here to some degree, but definitely leaning in the direction that allowing a function to throw multiple error types is better.

The primary reason I lean this way is that it enables more re-use of standard error types. Custom error types for an API often make sense, but not always. I am concerned about the need to create them just because our API contract might reasonably include two or three of the standard error types. Adding new types when they are not necessary introduces complexity and cognitive overhead. It also complicates catching of errors if the new custom type is a two or three case enum that just embeds the underlying error.

These problems will lead many people to just revert to an untyped throws clause. Objections to typed errors along these lines are common and legitimate. They will arise during review. It is best if you address them in the proposal now in order to focus a review on your solutions. My personal opinion is that allowing multiple error types and including a mechanism to perform automatic wrapping when appropriate would go a long way towards solving them.

Implementation challenges related to multi-typed errors have been discussed on the list quite a bit already. They would obviously need to be addressed if we go in that direction. I don’t want to downplay those. But I do think we need to try to identify the most usable solution for typed errors that we can first and then focus on implementation details. If the design needs to be modified to accommodate implementation at least we will have a better idea of what we are giving up.

I am willing to be convinced that a single error type is better than multiple error types but the current proposal does not provide a compelling argument in that direction. It just says “Java checked exceptions”. I know these have been pretty much universally considered a serious design mistake. My impression is that there are quite a few reasons for that. I don’t have any direct experience with Java and am not familiar with the details. If you could elaborate on specifically why you believe allowing multiple error types was a significant contributor to the problem in a manner that indicates that they will be a problem in any language that includes them I would appreciate that. Links would be sufficient if they are focused on answering this particular question.

I’m looking forward to your feedback on these thoughts.

Thanks,
Matthew

On Dec 18, 2015, at 1:29 AM, David Owens II via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

This a significantly updated proposal for typed annotations on the `throws` construct. The previous was closed due to not be complete; I believe I’ve addressed all of those concerns.

https://github.com/owensd/swift-evolution/blob/master/proposals/allow-type-annotations-on-throw.md

Allow Type Annotation on Throws
Proposal: SE-NNNN <>
Author(s): David Owens II <>
Status: Pending Approval for Review
Review manager: TBD
Introduction
The error handling system within Swift today creates an implicitly loose contract on the API. While this can be desirable in some cases, it’s certainly not desired in all cases. This proposal looks at modifying how the error handling mechanism works today by adding the ability to provide a strong API contract.

Error Handling State of the Union
This document will use the terminology and the premises defined in the Error Handling Rationale <https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst> document.

To very briefly summarize, there are four basic classification of errors:

Simple Domain Errors
Recoverable Errors
Universal Errors
Logic Failures
Each of these types of errors are handled differently at the call sites. Today, only the first two are directly handled by Swift error handling mechanism. The second two are uncatchable in Swift (such as fatalError(), ObjC exceptions, and force-unwrapping of null optionals).

Simple Domain Errors

As stated in Error Handling Rationale <https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst> document, the “Swift way” to handle such errors is to return an Optional<T>.

func parseInt(value: String) -> Int? {}
The simple fact of the result being Optional.None signifies that the string could not be parsed and converted into an Int. No other information is necessary or warranted.

Recoverable Errors

In this context, these are errors that need to provide additional information to the caller. The caller can then decide a course of action that needs to be taken. This could be any number of things, including, but not limited to, logging error information, attempting a retry, or potentially invoking a different code path. All of these errors implement the ErrorType protocol.

func openFile(filename: String) throws {}
The throws keyword annotates that the function can return additional error information. The caller must also explicitly make use of this when invoking the function.

do {
  try openFile("path/to/somewhere")
}
catch {}
Errors are able to propagate if called within another context that can throw, thus alleviating the annoying “catch and rethrow” behavior:

func parent() throws {
  try openFile("path/to/somwhere")
}
Lastly, functions can be marked to selectively throw errors if they take a function parameter that throws with the rethrows keyword. The really interesting part is that it’s only necessary to use try when calling the function with a throwing closure.

func openFile(filename: String) throws {}
func say(message: String) {}

func sample(fn: (_: String) throws -> ()) rethrows {
    try fn("hi")
}

try sample(openFile)
sample(say)
Converting Recoverable Errors to Domain Errors

Swift also has the try? construct. The notable thing about this construct is that it allows the caller to turn a “Recoverable Error” into a “Simple Domain Error”.

if let result = try? openFile("") {}
ErrorType Implementors

Errors are implemented using the ErrorType protocol. Since it is a protocol, new error types can be a class, a struct, or an enum. A type qualified throws clause would allow code authors to change the way that the catch-clauses need to be structured.

Enum Based ErrorType

When enums are used as the throwing mechanism, a generic catch-clause is still required as the compiler doesn’t have enough information. This leads to ambiguous code paths.

enum Errors: ErrorType {
    case OffBy1
    case MutatedValue
}

func f() throws { throw Errors.OffBy1 }

do {
    try f()
}
catch Errors.OffBy1 { print("increment by 1") }
catch Errors.MutatedValue { fatalError("data corrupted") }
The above code requires a catch {} clause, but it’s ambiguous what that case should do. There is no right way to handle this error. If the error is ignored, we’re now in the land of “Logic Errors”; the code path should never be hit. If we use a fatalError() construct, then we are now in the land of converting a potential compiler error into a “Universal Error”.

Both of these are undesirable.

Struct and Class Based ErrorType

In the current design, errors that are thrown require a catch-all all the time. In the proposed design, which will be explained further, a catch-all would not be required if there was a case-clause that matched the base type.

class ErrorOne: ErrorType {}
func g() throws { throw ErrorOne() }

do {
    try g()
}
catch is ErrorOne { print("ErrorOne") }
The advantage in these cases are different, these cases do not allow pattern matching over the error type members (as you can in a switch-statement, for example).

The workaround for this functionality is this:

class ErrorOne: ErrorType {
    let value: Int
    init(_ value: Int) { self.value = value }
}

do {
    try g()
}
catch {
    if let e = error as? ErrorOne {
        switch e {
        case _ where e.value == 0: print("0")
        case _ where e.value == 1: print("1")
        default: print("nothing")
        }
    }
}
This proposal would turn the above into:

class ErrorOne: ErrorType {
    let value: Int
    init(_ value: Int) { self.value = value }
}

do {
    try g()
}
catch _ where error.value == 0 { print("0") }
catch _ where error.value == 1 { print("1") }
catch { print("nothing") }
}
No gymnastics to go through, just straight-forward pattern-matching like you’d expect.

NOTE: This requires the promotion of the error constant to be allowed through the entirety of the catch-clauses.

Overriding

In the context of types, it’s completely possible to override functions with the throws annotations. The rules simply follow the rules today: covariance on the return type is allowed, contravariance is not.

Generics

When looking at generics, I cannot come up with a reason why they shouldn’t just work as normal:

func gen<SomeError: ErrorType>() throws SomeError {}
The only constraint would be that the specified error type must adhere to the ErrorType protocol. However, this is no different than today:

func f<T>(a: T) throws { throw a }
This results in the compiler error:

Thrown expression type ’T’ does not conform to ‘ErrorType’
This seems like it should “just work”.

Design Change Proposal
The design change is simple and straight-forward: allow for the annotation of the type of error that is being returned as an optional restriction. The default value would still be ErrorType.

func specific() throws MyError {}
func nonspecific() throws {}
There is a secondary result of this proposal: the error constant should be promoted to be allowed for use through-out all of the catch-clauses.

Impact on Existing Code

This is a non-breaking change. All existing constructs work today without change. That said, there are a few places where this change will have an impact on future usage.

Function Declarations

When a function has a throws clause that is attributed with a type, then that type becomes part of the function signature. This means that these two functions are not considered to be of the same type:

func one() throws {}
func two() throws NumberError {}
The function signatures are covariant though, so either one or two can be assigned to f below:

let f: () throws -> ()
This is completely fine as NumberError still implements the ErrorType protocol.

However, in this case:

let g: () throws NumberError -> ()
It would not be valid to assign one to g as the type signature is more specific.

throws and rethrows

Functions currently have the ability to be marked as rethrows. This basically says that if a closure parameter can throw, then the function will throw too.

func whatever(fn: () throws -> ()) rethrows {}
The whatever function is up for anything that fn is up for. Keeping in line with this mentality, the rethrows would exhibit the same behavior: typed annotations simply apply if present and do not if they are missing.

func specific(fn: () throws HappyError -> ()) rethrows {}
This all works as expected:

func f() throws HappyError {}
func g() {}

try specific(f)
specific(g)
This works for the same covariant reason as the non-qualified throws implementation works: a non-throwing function is always able to be passed in for a throwing function.

The do-catch statement

There are two rule changes here, but again, it’s non-breaking.

The first rule change is to promote the error constant that would normally only be allowed in the catch-all clause (no patterns) to be available throughout each of the catch clauses. This allows for the error information to be used in pattern matching, which is especially valuable in the non-enum case.

The second change is to allow the error constant to take on a specific type when all of the throwing functions throw the same specified type. When this is the case, two things become possible:

In the enum-type implementation of ErrorType, the catch-clauses can now be exhaustive.
In the all of the cases, the API of the specific ErrorType becomes available in the catch-clause without casting the error constant. This greatly simplifies the pattern-matching process.
In the case that there are heterogenous ErrorType implementations being returned, the errorconstant simply has the type of ErrorType.

The try call sites

There is no change for the try, try?, or try! uses. The only clarification I’ll add is that try?is still the appropriate way to promote an error from a “Recoverable Error” to a “Simple Domain Error”.

Alternate Proposals
There is another common error handling mechanism used in the community today: Either<L, R>. There are various implementations, but they all basically boil down to an enum that captures the value or the error information.

I actually consider my proposal syntactic sugar over this concept. If and when Swift supports covariant generics, there is not a significant reason I can see why the underlying implementation could not just be that.

The advantage is that the proposed (and existing) syntax of throws greatly increases the readability and understanding that this function actually possesses the ability to throw errors and they should be handled.

The other advantage of this syntax is that it doesn’t require a new construct to force the usage of the return type.

Further, if functions where to ever gain the ability to be marked as async, this could now be handled naturally within the compiler as the return type could a promise-like implementation for those.

Criticisms
From the earlier threads on the swift-evolution mailing list, there are a few primary points of contention about this proposal.

Aren’t we just creating Java checked-exceptions, which we all know are terrible?

No. The primary reason is that a function can only return a single error-type. The other major reason is that the error philosophy is very different in Swift than in Java.

Aren’t we creating fragile APIs that can cause breaking changes?

Potentially, yes. This depends on how the ABI is handled in Swift 3 for enums. The same problem exists today, although at a lesser extent, for any API that returns an enum today.

Chris Lattner mentioned this on the thread:

The resilience model addresses how the public API from a module can evolve without breaking clients (either at the source level or ABI level). Notably, we want the ability to be able to add enum cases to something by default, but also to allow API authors to opt into more performance/strictness by saying that a public enum is “fragile” or “closed for evolution”.
So if enums have an attribute that allows API authors to denote the fragility enums, then this can be handled via that route.

Another potential fix is that only internal and private scoped functions are allowed to use the exhaustive-style catch-clauses. For all public APIs, they would still need the catch-all clauses.

For APIs that return non-enum based ErrorType implementations, then no, this does not contribute to the fragility problem.

Aren’t we creating the need for wrapper errors?

This is a philosophical debate. I’ll simply state that I believe that simply re-throwing an error, say some type of IO error, from your API that is not an IO-based API is design flaw: you are exposing implementation details to users. This creates a fragile API surface.

Also, since the type annotation is opt-in, I feel like this is a really minor argument. If your function is really able to throw errors from various different API calls, then just stick with the default ErrorType.

_______________________________________________
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 <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #10

David,

Thank you for taking the time to continue working on a proposal for typed throws. I agree that this feature is very desirable and appreciate the work you’re doing to bring forward a proposal. I think it’s a great start but also has some room for improvement.

First, I think it could be strengthened by incorporating some learning from Rust. My impression is that the Rust community is very happy with typed error handling. Adding some detail about their experience would provide a counter-example to those who are concerned about the experience in Java and C++.

I’m not involved in the Rust community so I wouldn’t feel comfortable making claims for them.

I’m not asking for you to speak for them. But I do think we need to learn from communities that are having success with typed error handling. Your proposal would be stronger if it went into detail about how it would avoid the problems that have been encountered in other languages. The experience of Rust could help to make that case as it is concrete and not hypothetical.

I agree that error types are an important part of an API contract. One of the big hurdles to doing this well is the need to catch errors when all that needs to be done is to wrap and rethrow them. Ideally should not need to do this just to perform a simple type translation to map the underlying error into the type we wish to expose as part of a stable API contract. You might want to take a look at the From mechanism Rust uses to facilitate this. IMO a proposal for typed error handling should address this issue in some way (even if the author determines this mechanism is not necessary or a good design cannot be identified).

The From() construct seems like a map(T) -> U problem, but it seems heavily tied into the ability to create sum types. Swift doesn’t have this feature, and that feature is out-of-scope for this proposal. More on this later.

My understanding is that Rust uses static multi-dispatch to do this. I don’t believe it has anything to do with structural sum types. Rust error handling uses a Result type with a single error case: http://doc.rust-lang.org/book/error-handling.html.

If you don’t believe a mechanism like this is necessary and don’t include one in your design it will lead to either a lot of boilerplate or avoidance of typed errors (depending on the developer). I think you should expect a lot of complaints about this and I think it will generate a lot of -1 votes for the proposal.

I would also like to see much more detail on why you think allowing a function to throw multiple error types is problematic. My impression is that you have concerns from a usability point of view. I am on the fence here to some degree, but definitely leaning in the direction that allowing a function to throw multiple error types is better.

Sure. There’s no functionality today to auto-generate a sum type in Swift today, and that is what this request really is. If you want to return multiple return types, then you need to do exactly what Rust does and create a sum type that composes the various types of errors. This exposes the same potential fragile API surface as extending enums do. I did call this part out specifically.

How does this create a fragile API surface area? Adding a new error type to the signature would be a breaking change to the API contract. This is really no different than changing the type of error that can be thrown under your proposal.

I see this functionality as a general limitation in the language. For example, errors are not the only context where you may want to return a type of A, B, or C. There have been other proposals on how we might do that in Swift. If and when it was solved in the general case for type parameters, I can’t foresee a compelling reason why it wouldn’t work in this context as well.

That makes sense in some ways, but I don’t think it’s unreasonable to ask for some analysis of whether a better design for typed errors would be possible if we had them. IMO it’s pretty important to get the design of typed errors right if / when we add them. If we don’t it will be considered a major mistake and will lead to a lot of less than desirable outcomes down the road.

I also think typed errors may be one of the more important use cases for structural sum types of some kind. If we are able to show that design problems that cannot be solved without them can be solved with them that might influence whether they are added or not. It might also influence when it makes sense to add support for typed errors to the language.

I am willing to be convinced that a single error type is better than multiple error types but the current proposal does not provide a compelling argument in that direction. It just says “Java checked exceptions”. I know these have been pretty much universally considered a serious design mistake. My impression is that there are quite a few reasons for that. I don’t have any direct experience with Java and am not familiar with the details. If you could elaborate on specifically why you believe allowing multiple error types was a significant contributor to the problem in a manner that indicates that they will be a problem in any language that includes them I would appreciate that. Links would be sufficient if they are focused on answering this particular question.

I guess I should have just specifically called this out in the proposal. It’s not because of “Java checked exceptions”, it’s because nowhere else in the language are types allowed to be essentially annotated in a sum-like fashion. We can’t directly say a function returns an Int or a String. We can’t say a parameter can take an Int or a Double. Similarly, I propose we can’t say a function can return an error A or B.

Thus, the primary reason is about type-system consistency.

Swift already supports a construct to create sum types: associated enums. What it doesn’t allow is the ability to create them in a syntactic shorthand. In this way, my error proposal does the same thing as Rust: multiple return types need to be combined into a single-type - enum.

That approach would make catch clauses rather clunky by nesting errors inside of associated values. If you’re advocating for this approach do you have any ideas on how to streamline syntax for catching them?

Matthew

···

On Dec 18, 2015, at 10:50 AM, David Owens II <david@owensd.io> wrote:

On Dec 18, 2015, at 5:53 AM, Matthew Johnson <matthew@anandabits.com> wrote:


(Félix Cloutier) #11

I'm not sure I understand your comment either. If I understand correctly, you say that the problem I describe is applicable only to polymorphic types (which is true). However, you then say that the only option today is polymorphic error types. Isn't that an issue? (Also, why is it the only option today?)

···

Le 18 déc. 2015 à 11:58:44, David Owens II <david@owensd.io> a écrit :

On Dec 18, 2015, at 7:03 AM, Félix Cloutier <felixcca@yahoo.ca> wrote:

For this reason, I don't like to encourage throwing polymorphic types, and I think that it's a misconception to pretend that having a single type in the throws annotation ensures that the function throws a single type. In my opinion, the fact that it's currently possible but awkward to use polymorphic types as errors is exactly as much support as the feature should receive.

I don’t follow this. Declaring a type that is an enum or a struct absolutely guarantees that the function only returns a single type. If the type is a class-based error, then sure, there’s not guarantee.

However, the only option today is polymorphic error types.

-David


(Dennis Lysenko) #12

Sorry Matthew, I think my email is having delivery issues because I didn't
see any of those messages when I sent mine, though I see them timestamped
before my message now.

Yes, Felix, those were valid concerns. But, as mentioned before, what we
have now is the most polymorphic thing possible. All those functions throw
ErrorType and that can literally be anything imaginable.

What could solve the issue is if type-annotated throws only allowed final
(closed) classes. Also, if I'm not mistaken, enums are sealed and cannot be
extended so they are a fantastic candidate for avoiding the proliferation
of extending error classes. Maybe it could be limited to enums.

···

On Fri, Dec 18, 2015, 10:40 AM Matthew Johnson <matthew@anandabits.com> wrote:

On Dec 18, 2015, at 9:25 AM, Dennis Lysenko <dennis.s.lysenko@gmail.com> > wrote:

Genuinely, David, thank you for taking up the mantle of this problem. To
me, the lack of type annotations makes the error handling painful and I
wish the team just hadn't released it until the design was smoothed out
fully. Those dangling catch-all blocks when I've caught all cases make it
uselessly verbose and moreover do not fit with the language at all.

As for the multiple vs. single type annotations, I do think that you need
a concrete example of why this will be different from java to quell the
concerns of all the people that skim the proposal, don't think about it,
and exclaim "but everyone hates it in Java!" so I agree on the singular
type annotations.

I definitely agree that we need to address the Java problem. That said, I
don’t think avoiding knee-jerk objection from people who don’t read and
consider a proposal carefully is a good way to approach design. There may
be good reasons to choose singular type annotations but this is not one of
them.

I believe Félix raised good points about polymorphism and complex
hierarchy causing problems in Java. That sounds like it is the source of
at least a significant part of the problems with Java’s checked exception
model.

Also, your point about being able to mark functions async strongly
supports single type annotations until we get union types (if ever).

I am glad you addressed covariance/contravariance and the semantics of
function types when they throw general errors vs. a specific one.

One question, mainly to the compiler team: would it be reasonable to be
able to use generics covariant over the throws operator? For example, I
could define a function that takes a function which throws E and returns R,
and creates a function that takes a callback which takes an argument of
type Either<R, E> instead. This would be an incredibly powerful feature.

On Fri, Dec 18, 2015, 8:53 AM Matthew Johnson via swift-evolution < > swift-evolution@swift.org> wrote:

David,

Thank you for taking the time to continue working on a proposal for typed
throws. I agree that this feature is very desirable and appreciate the
work you’re doing to bring forward a proposal. I think it’s a great start
but also has some room for improvement.

First, I think it could be strengthened by incorporating some learning
from Rust. My impression is that the Rust community is very happy with
typed error handling. Adding some detail about their experience would
provide a counter-example to those who are concerned about the experience
in Java and C++.

I agree that error types are an important part of an API contract. One
of the big hurdles to doing this well is the need to catch errors when all
that needs to be done is to wrap and rethrow them. Ideally should not need
to do this just to perform a simple type translation to map the underlying
error into the type we wish to expose as part of a stable API contract.
You might want to take a look at the From mechanism Rust uses to facilitate
this. IMO a proposal for typed error handling should address this issue in
some way (even if the author determines this mechanism is not necessary or
a good design cannot be identified).

I would also like to see much more detail on why you think allowing a
function to throw multiple error types is problematic. My impression is
that you have concerns from a usability point of view. I am on the fence
here to some degree, but definitely leaning in the direction that allowing
a function to throw multiple error types is better.

The primary reason I lean this way is that it enables more re-use of
standard error types. Custom error types for an API often make sense, but
not always. I am concerned about the need to create them just because our
API contract might reasonably include two or three of the standard error
types. Adding new types when they are not necessary introduces complexity
and cognitive overhead. It also complicates catching of errors if the new
custom type is a two or three case enum that just embeds the underlying
error.

These problems will lead many people to just revert to an untyped throws
clause. Objections to typed errors along these lines are common and
legitimate. They will arise during review. It is best if you address them
in the proposal now in order to focus a review on your solutions. My
personal opinion is that allowing multiple error types and including a
mechanism to perform automatic wrapping when appropriate would go a long
way towards solving them.

Implementation challenges related to multi-typed errors have been
discussed on the list quite a bit already. They would obviously need to be
addressed if we go in that direction. I don’t want to downplay those. But
I do think we need to try to identify the most usable solution for typed
errors that we can first and then focus on implementation details. If the
design needs to be modified to accommodate implementation at least we will
have a better idea of what we are giving up.

I am willing to be convinced that a single error type is better than
multiple error types but the current proposal does not provide a compelling
argument in that direction. It just says “Java checked exceptions”. I
know these have been pretty much universally considered a serious design
mistake. My impression is that there are quite a few reasons for that. I
don’t have any direct experience with Java and am not familiar with the
details. If you could elaborate on specifically why you believe allowing
multiple error types was a significant contributor to the problem in a
manner that indicates that they will be a problem in any language that
includes them I would appreciate that. Links would be sufficient if they
are focused on answering this particular question.

I’m looking forward to your feedback on these thoughts.

Thanks,
Matthew

On Dec 18, 2015, at 1:29 AM, David Owens II via swift-evolution < >> swift-evolution@swift.org> wrote:

This a significantly updated proposal for typed annotations on the
`throws` construct. The previous was closed due to not be complete; I
believe I’ve addressed all of those concerns.

https://github.com/owensd/swift-evolution/blob/master/proposals/allow-type-annotations-on-throw.md

Allow Type Annotation on Throws

   - Proposal: SE-NNNN
   - Author(s): David Owens II
   - Status: *Pending Approval for Review*
   - Review manager: TBD

Introduction

The error handling system within Swift today creates an implicitly loose
contract on the API. While this can be desirable in some cases, it’s
certainly not desired in *all* cases. This proposal looks at modifying
how the error handling mechanism works today by adding the ability to
provide a strong API contract.
Error Handling State of the Union

This document will use the terminology and the premises defined in the Error
Handling Rationale
<https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst>
document.

To very briefly summarize, there are four basic classification of errors:

   1. Simple Domain Errors
   2. Recoverable Errors
   3. Universal Errors
   4. Logic Failures

Each of these types of errors are handled differently at the call sites.
Today, only the first two are directly handled by Swift error handling
mechanism. The second two are uncatchable in Swift (such as fatalError(),
ObjC exceptions, and force-unwrapping of null optionals).
Simple Domain Errors

As stated in Error Handling Rationale
<https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst> document,
the “Swift way” to handle such errors is to return an Optional<T>.

func parseInt(value: String) -> Int? {}

The simple fact of the result being Optional.None signifies that the
string could not be parsed and converted into an Int. No other
information is necessary or warranted.
Recoverable Errors

In this context, these are errors that need to provide additional
information to the caller. The caller can then decide a course of action
that needs to be taken. This could be any number of things, including, but
not limited to, logging error information, attempting a retry, or
potentially invoking a different code path. All of these errors implement
the ErrorType protocol.

func openFile(filename: String) throws {}

The throws keyword annotates that the function can return additional
error information. The caller must also explicitly make use of this when
invoking the function.

do {
  try openFile("path/to/somewhere")
}
catch {}

Errors are able to propagate if called within another context that can
throw, thus alleviating the annoying “catch and rethrow” behavior:

func parent() throws {
  try openFile("path/to/somwhere")
}

Lastly, functions can be marked to selectively throw errors if they take
a function parameter that throws with the rethrows keyword. The really
interesting part is that it’s only necessary to use try when calling the
function with a throwing closure.

func openFile(filename: String) throws {}
func say(message: String) {}

func sample(fn: (_: String) throws -> ()) rethrows {
    try fn("hi")
}

try sample(openFile)
sample(say)

Converting Recoverable Errors to Domain Errors

Swift also has the try? construct. The notable thing about this
construct is that it allows the caller to turn a “Recoverable Error” into a
“Simple Domain Error”.

if let result = try? openFile("") {}

ErrorType Implementors

Errors are implemented using the ErrorType protocol. Since it is a
protocol, new error types can be a class, a struct, or an enum. A type
qualified throws clause would allow code authors to change the way that
the catch-clauses need to be structured.
Enum Based ErrorType

When enums are used as the throwing mechanism, a generic catch-clause is
still required as the compiler doesn’t have enough information. This leads
to ambiguous code paths.

enum Errors: ErrorType {
    case OffBy1
    case MutatedValue
}

func f() throws { throw Errors.OffBy1 }

do {
    try f()
}
catch Errors.OffBy1 { print("increment by 1") }
catch Errors.MutatedValue { fatalError("data corrupted") }

The above code requires a catch {} clause, but it’s ambiguous what that
case should do. There is no *right* way to handle this error. If the
error is ignored, we’re now in the land of “Logic Errors”; the code path
should never be hit. If we use a fatalError() construct, then we are now
in the land of converting a potential compiler error into a “Universal
Error”.

Both of these are undesirable.
Struct and Class Based ErrorType

In the current design, errors that are thrown require a catch-all all the
time. In the proposed design, which will be explained further, a catch-all
would not be required if there was a case-clause that matched the base type.

class ErrorOne: ErrorType {}
func g() throws { throw ErrorOne() }

do {
    try g()
}
catch is ErrorOne { print("ErrorOne") }

The advantage in these cases are different, these cases do not allow
pattern matching over the error type members (as you can in a
switch-statement, for example).

The workaround for this functionality is this:

class ErrorOne: ErrorType {
    let value: Int
    init(_ value: Int) { self.value = value }
}

do {
    try g()
}
catch {
    if let e = error as? ErrorOne {
        switch e {
        case _ where e.value == 0: print("0")
        case _ where e.value == 1: print("1")
        default: print("nothing")
        }
    }
}

This proposal would turn the above into:

class ErrorOne: ErrorType {
    let value: Int
    init(_ value: Int) { self.value = value }
}

do {
    try g()
}
catch _ where error.value == 0 { print("0") }
catch _ where error.value == 1 { print("1") }
catch { print("nothing") }
}

No gymnastics to go through, just straight-forward pattern-matching like
you’d expect.

NOTE: This requires the promotion of the error constant to be allowed
through the entirety of the catch-clauses.
Overriding

In the context of types, it’s completely possible to override functions
with the throws annotations. The rules simply follow the rules today:
covariance on the return type is allowed, contravariance is not.
Generics

When looking at generics, I cannot come up with a reason why they
shouldn’t just work as normal:

func gen<SomeError: ErrorType>() throws SomeError {}

The only constraint would be that the specified error type must adhere to
the ErrorType protocol. However, this is no different than today:

func f<T>(a: T) throws { throw a }

This results in the compiler error:

Thrown expression type ’T’ does not conform to ‘ErrorType’

This seems like it should “just work”.
Design Change Proposal

The design change is simple and straight-forward: allow for the
annotation of the type of error that is being returned as an optional
restriction. The default value would still be ErrorType.

func specific() throws MyError {}
func nonspecific() throws {}

There is a secondary result of this proposal: the error constant should
be promoted to be allowed for use through-out all of the catch-clauses.
Impact on Existing Code

This is a non-breaking change. All existing constructs work today without
change. That said, there are a few places where this change will have an
impact on future usage.
Function Declarations

When a function has a throws clause that is attributed with a type, then
that type becomes part of the function signature. This means that these two
functions are not considered to be of the same type:

func one() throws {}
func two() throws NumberError {}

The function signatures are covariant though, so either one or two can
be assigned to f below:

let f: () throws -> ()

This is completely fine as NumberError still implements the ErrorType
protocol.

However, in this case:

let g: () throws NumberError -> ()

It would not be valid to assign one to g as the type signature is more
specific.
throws and rethrows

Functions currently have the ability to be marked as rethrows. This
basically says that if a closure parameter can throw, then the function
will throw too.

func whatever(fn: () throws -> ()) rethrows {}

The whatever function is up for anything that fn is up for. Keeping in
line with this mentality, the rethrows would exhibit the same behavior:
typed annotations simply apply if present and do not if they are missing.

func specific(fn: () throws HappyError -> ()) rethrows {}

This all works as expected:

func f() throws HappyError {}
func g() {}

try specific(f)
specific(g)

This works for the same covariant reason as the non-qualified throws implementation
works: a non-throwing function is always able to be passed in for a
throwing function.
The do-catch statement

There are two rule changes here, but again, it’s non-breaking.

The first rule change is to promote the error constant that would
normally only be allowed in the catch-all clause (no patterns) to be
available throughout each of the catch clauses. This allows for the error
information to be used in pattern matching, which is especially valuable in
the non-enum case.

The second change is to allow the error constant to take on a specific
type when *all* of the throwing functions throw the same specified type.
When this is the case, two things become possible:

   1. In the enum-type implementation of ErrorType, the catch-clauses
   can now be exhaustive.
   2. In the all of the cases, the API of the specific ErrorType becomes
   available in the catch-clause without casting the error constant.
   This greatly simplifies the pattern-matching process.

In the case that there are heterogenous ErrorType implementations being
returned, the errorconstant simply has the type of ErrorType.
The try call sites

There is no change for the try, try?, or try! uses. The only
clarification I’ll add is that try?is still the appropriate way to
promote an error from a “Recoverable Error” to a “Simple Domain Error”.
Alternate Proposals

There is another common error handling mechanism used in the community
today: Either<L, R>. There are various implementations, but they all
basically boil down to an enum that captures the value or the error
information.

I actually consider my proposal syntactic sugar over this concept. If and
when Swift supports covariant generics, there is not a significant reason I
can see why the underlying implementation could not just be that.

The advantage is that the proposed (and existing) syntax of throws greatly
increases the readability and understanding that this function actually
possesses the ability to throw errors and they should be handled.

The other advantage of this syntax is that it doesn’t require a new
construct to force the usage of the return type.

Further, if functions where to ever gain the ability to be marked as
async, this could now be handled naturally within the compiler as the
return type could a promise-like implementation for those.
Criticisms

From the earlier threads on the swift-evolution mailing list, there are a
few primary points of contention about this proposal.
Aren’t we just creating Java checked-exceptions, which we all know are
terrible?

No. The primary reason is that a function can only return a single
error-type. The other major reason is that the error philosophy is very
different in Swift than in Java.
Aren’t we creating fragile APIs that can cause breaking changes?

Potentially, yes. This depends on how the ABI is handled in Swift 3 for
enums. The same problem exists today, although at a lesser extent, for any
API that returns an enum today.

Chris Lattner mentioned this on the thread:

The resilience model addresses how the public API from a module can
evolve without breaking clients (either at the source level or ABI level).
Notably, we want the ability to be able to add enum cases to something by
default, but also to allow API authors to opt into more
performance/strictness by saying that a public enum is “fragile” or “closed
for evolution”.

So if enums have an attribute that allows API authors to denote the
fragility enums, then this can be handled via that route.

Another potential fix is that *only* internal and private scoped
functions are allowed to use the exhaustive-style catch-clauses. For all
public APIs, they would still need the catch-all clauses.

For APIs that return non-enum based ErrorType implementations, then no,
this does not contribute to the fragility problem.
Aren’t we creating the need for wrapper errors?

This is a philosophical debate. I’ll simply state that I believe that
simply re-throwing an error, say some type of IO error, from your API that
is not an IO-based API is design flaw: you are exposing implementation
details to users. This creates a fragile API surface.

Also, since the type annotation is opt-in, I feel like this is a really
minor argument. If your function is really able to throw errors from
various different API calls, then just stick with the default ErrorType.
_______________________________________________
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


(Alex Popov) #13

Hi all,

I am very much pro this addition, if for nothing else than more information
for the developer using the API.

Limiting `throws MyErrorType` to only Enums, Structs and final Classes gets
around the Java problems so far as I can see.

As for the usefulness of this, I'm working on a fairly large project, and the
only way of documenting what errors can be thrown is through documentation
comments, which must then be maintained — there is little worse than incorrect
/out-of-date documentation. Although currently the catch-all is mandated,
guaranteeing safety, as a developer I'm equally concerned about recovering
from an error: without knowing exactly which Enum can be thrown, or what
subset of cases can occur, I either have to navigate to the source code and go
down the rabbit hole of tracking that information down, or just accepting that
I don't know what'll be thrown (and thereby potentially not recovering from a
recoverable failure).

I find it really frustrating that the compiler is unable to help me out with
this, even though in certain cases I _can_ guarantee what will be thrown. If
this proposal also brings about more convenient pattern matching for error
handling, that's a +2 from me.

<br

···

<br

Alex Popov Jr.

Principal iOS Developer | Shelfie

On Dec 18 2015, at 9:42 am, Matthew Johnson via swift-evolution &lt;swift- evolution@swift.org&gt; wrote:
  

On Dec 18, 2015, at 10:50 AM, David Owens II &lt;[david@owensd.io](mailto:david@owensd.io)&gt; wrote:

On Dec 18, 2015, at 5:53 AM, Matthew Johnson &lt;[matthew@anandabits.com](mailto:matthew@anandabits.com)&gt; wrote:
  
David,
  
Thank you for taking the time to continue working on a proposal for typed
throws. I agree that this feature is very desirable and appreciate the work
you’re doing to bring forward a proposal. I think it’s a great start but also
has some room for improvement.
  
First, I think it could be strengthened by incorporating some learning from
Rust. My impression is that the Rust community is very happy with typed error
handling. Adding some detail about their experience would provide a counter-
example to those who are concerned about the experience in Java and C++.

I’m not involved in the Rust community so I wouldn’t feel comfortable making
claims for them.

I’m not asking for you to speak for them. But I do think we need to learn

from communities that are having success with typed error handling. Your
proposal would be stronger if it went into detail about how it would avoid the
problems that have been encountered in other languages. The experience of
Rust could help to make that case as it is concrete and not hypothetical.

I agree that error types are an important part of an API contract. One of

the big hurdles to doing this well is the need to catch errors when all that
needs to be done is to wrap and rethrow them. Ideally should not need to do
this just to perform a simple type translation to map the underlying error
into the type we wish to expose as part of a stable API contract. You might
want to take a look at the From mechanism Rust uses to facilitate this. IMO a
proposal for typed error handling should address this issue in some way (even
if the author determines this mechanism is not necessary or a good design
cannot be identified).

The From() construct seems like a map(T) -&gt; U problem, but it seems heavily
tied into the ability to create sum types. Swift doesn’t have this feature,
and that feature is out-of-scope for this proposal. More on this later.

My understanding is that Rust uses static multi-dispatch to do this. I

don’t believe it has anything to do with structural sum types. Rust error
handling uses a Result type with a single error case: <http://doc.rust-
lang.org/book/error-handling.html>.

If you don’t believe a mechanism like this is necessary and don’t include

one in your design it will lead to either a lot of boilerplate or avoidance of
typed errors (depending on the developer). I think you should expect a lot of
complaints about this and I think it will generate a lot of -1 votes for the
proposal.

I would also like to see much more detail on why you think allowing a

function to throw multiple error types is problematic. My impression is that
you have concerns from a usability point of view. I am on the fence here to
some degree, but definitely leaning in the direction that allowing a function
to throw multiple error types is better.

Sure. There’s no functionality today to auto-generate a sum type in Swift
today, and that is what this request really is. If you want to return multiple
return types, then you need to do exactly what Rust does and create a sum type
that composes the various types of errors. This exposes the same potential
fragile API surface as extending enums do. I did call this part out
specifically.

How does this create a fragile API surface area? Adding a new error type to

the signature would be a breaking change to the API contract. This is really
no different than changing the type of error that can be thrown under your
proposal.

I see this functionality as a general limitation in the language. For

example, errors are not the only context where you may want to return a type
of A, B, or C. There have been other proposals on how we might do that in
Swift. If and when it was solved in the general case for type parameters, I
can’t foresee a compelling reason why it wouldn’t work in this context as
well.

That makes sense in some ways, but I don’t think it’s unreasonable to ask

for some analysis of whether a better design for typed errors would be
possible if we had them. IMO it’s pretty important to get the design of typed
errors right if / when we add them. If we don’t it will be considered a major
mistake and will lead to a lot of less than desirable outcomes down the road.

I also think typed errors may be one of the more important use cases for

structural sum types of some kind. If we are able to show that design
problems that **cannot** be solved without them **can** be solved with them
that might influence whether they are added or not. It might also influence
**when** it makes sense to add support for typed errors to the language.

I am willing to be convinced that a single error type is better than

multiple error types but the current proposal does not provide a compelling
argument in that direction. It just says “Java checked exceptions”. I know
these have been pretty much universally considered a serious design mistake.
My impression is that there are quite a few reasons for that. I don’t have
any direct experience with Java and am not familiar with the details. If you
could elaborate on specifically why you believe allowing multiple error types
was a significant contributor to the problem in a manner that indicates that
they will be a problem in any language that includes them I would appreciate
that. Links would be sufficient if they are focused on answering this
particular question.

I guess I should have just specifically called this out in the proposal. It’s
not because of “Java checked exceptions”, it’s because nowhere else in the
language are types allowed to be essentially annotated in a sum-like fashion.
We can’t directly say a function returns an Int or a String. We can’t say a
parameter can take an Int or a Double. Similarly, I propose we can’t say a
function can return an error A or B.
  
Thus, the primary reason is about type-system consistency.
  
Swift already supports a construct to create sum types: associated enums. What
it doesn’t allow is the ability to create them in a syntactic shorthand. In
this way, my error proposal does the same thing as Rust: multiple return types
need to be combined into a single-type - enum.

That approach would make catch clauses rather clunky by nesting errors

inside of associated values. If you’re advocating for this approach do you
have any ideas on how to streamline syntax for catching them?

Matthew

![](https://u2002410.ct.sendgrid.net/wf/open?upn=CmwAv3oRa0AH4Hd1bWC6X-

2BzbhPqo1YEo6mPHEujr90vUljsud-2BB6KUDP5cz-
2Fv0xXzcKh2I7l3AVBGsasABqpSVQZA47MBrZnIRbuczKfFabYpw7SI-2FEE3vWg8ZsRzYrusFrkP3
-2Fdfov48EZIMr1UIxRUjZjSkWFw5gJSpy8pL-2Fo1jlRzHx1-2B7tEBTDx1EdAI9LfCCL-
2FD4vOGJSJXCTPis0shiFWB5ZEKHAD1LQ3af20-3D)


(David Owens II) #14

I’m not asking for you to speak for them. But I do think we need to learn from communities that are having success with typed error handling. Your proposal would be stronger if it went into detail about how it would avoid the problems that have been encountered in other languages. The experience of Rust could help to make that case as it is concrete and not hypothetical.

Sure, it could. It’s also anecdotal. It’s not necessarily true that something that works well in one context works well in another. It’s good to note that typed errors are wholly considered bad, but I’m not sure how much further we need to go then that. If you have specifics, then I could probably add them as an addendum to the proposal.

My understanding is that Rust uses static multi-dispatch to do this. I don’t believe it has anything to do with structural sum types. Rust error handling uses a Result type with a single error case: http://doc.rust-lang.org/book/error-handling.html.

That example takes you through many of the options available. In the end, you end up at the sum-type for the error:
fn search<P: AsRef<Path>>
         (file_path: &Option<P>, city: &str)
         -> Result<Vec<PopulationCount>, CliError> {
    ...
}
It’s the CliError which is defined as:
enum CliError {
    Io(io::Error),
    Csv(csv::Error),
    NotFound,
}
The From() function essentially allows the try! macro to expand these in a nicer way.

So back to the proposal, one of the key things is to promote the `error` constant throughout the catch-clauses. This means that we can already leverage Swift’s pattern matching to solve this problem:

enum Combined {
    case IO(String)
    case Number(Int)
}

func simulate(err: Combined) {
    switch err {
    case let Combined.IO(string) where string == "hi": print("only hi!")
    case let Combined.IO(string): print(string)
    case let Combined.Number(value): print(value)
    }
}

simulate(Combined.IO("hi"))
simulate(Combined.IO("io"))
simulate(Combined.Number(9))

It’s not hard to use Swift’s pattern matching to extract out the inner information on an associated value enum and white the case/catch clauses. So unless I’m missing something, I think Swift already provides a good mechanism to do what you’re asking for, with the caveat that the `error` constant is promoted to be usable in the catch-clauses similar to how the switch-statements work.

Maybe adding this to the proposal would clarify usage?

How does this create a fragile API surface area? Adding a new error type to the signature would be a breaking change to the API contract. This is really no different than changing the type of error that can be thrown under your proposal.

It’s the same fragility that enums create; this was covered in the criticisms section. The likelihood of adding additional error cases is much greater than a change that would completely change the type of the error.

I see this functionality as a general limitation in the language. For example, errors are not the only context where you may want to return a type of A, B, or C. There have been other proposals on how we might do that in Swift. If and when it was solved in the general case for type parameters, I can’t foresee a compelling reason why it wouldn’t work in this context as well.

That makes sense in some ways, but I don’t think it’s unreasonable to ask for some analysis of whether a better design for typed errors would be possible if we had them. IMO it’s pretty important to get the design of typed errors right if / when we add them. If we don’t it will be considered a major mistake and will lead to a lot of less than desirable outcomes down the road.

I also think typed errors may be one of the more important use cases for structural sum types of some kind. If we are able to show that design problems that cannot be solved without them can be solved with them that might influence whether they are added or not. It might also influence when it makes sense to add support for typed errors to the language.

The problem can be solved without implicitly generated sum types though. The design of typed errors, as proposed, is to be consistent with the Swift type system today. Regardless, I’ve added a response in the “cirticisms” section that hopefully addresses this in some manner - basically, yes it would be helpful, but out of scope for this proposal.

That approach would make catch clauses rather clunky by nesting errors inside of associated values. If you’re advocating for this approach do you have any ideas on how to streamline syntax for catching them?

See above example. Does that address this concern?

-David

···

On Dec 18, 2015, at 9:41 AM, Matthew Johnson <matthew@anandabits.com> wrote:


(Félix Cloutier) #15

Oh, I see what you mean. I considered polymorphic types to be class hierarchies, when you're talking about ErrorType polymorphism.

Yes, I think that the compiler should be aware of what the function can throw, but I would be happier if it stayed a bit inconvenient to use reference types.

···

Le 18 déc. 2015 à 12:17:06, Félix Cloutier via swift-evolution <swift-evolution@swift.org> a écrit :

I'm not sure I understand your comment either. If I understand correctly, you say that the problem I describe is applicable only to polymorphic types (which is true). However, you then say that the only option today is polymorphic error types. Isn't that an issue? (Also, why is it the only option today?)

Le 18 déc. 2015 à 11:58:44, David Owens II <david@owensd.io> a écrit :

On Dec 18, 2015, at 7:03 AM, Félix Cloutier <felixcca@yahoo.ca> wrote:

For this reason, I don't like to encourage throwing polymorphic types, and I think that it's a misconception to pretend that having a single type in the throws annotation ensures that the function throws a single type. In my opinion, the fact that it's currently possible but awkward to use polymorphic types as errors is exactly as much support as the feature should receive.

I don’t follow this. Declaring a type that is an enum or a struct absolutely guarantees that the function only returns a single type. If the type is a class-based error, then sure, there’s not guarantee.

However, the only option today is polymorphic error types.

-David

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


(Ricardo Parada) #16

Hi David

I started reading your proposal and I have a couple of questions.

In the Enum Base ErrorType example you mentioned that it requires a "catch { }" clause. However the code is already covering the two possible Enum values (OffBy1 and MutatedValue). Why is the "catch { }" required? I typed that code into a playground and I did not get any errors. Are you saying that because the Enum type could add a value in the future?

Also, you proposed the catch clause to use error as the name of the constant holding the error. Wouldn't it be better to let the programmer decide the name rather than hard coding it to use error? For example:

catch e where e.value == 0 { print("0") }
catch e where e.value == 1 { print("1") }
catch { print("nothing") }

Thank you
Ricardo

···

Sent from my iPhone

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

On Dec 18, 2015, at 9:41 AM, Matthew Johnson <matthew@anandabits.com> wrote:

I’m not asking for you to speak for them. But I do think we need to learn from communities that are having success with typed error handling. Your proposal would be stronger if it went into detail about how it would avoid the problems that have been encountered in other languages. The experience of Rust could help to make that case as it is concrete and not hypothetical.

Sure, it could. It’s also anecdotal. It’s not necessarily true that something that works well in one context works well in another. It’s good to note that typed errors are wholly considered bad, but I’m not sure how much further we need to go then that. If you have specifics, then I could probably add them as an addendum to the proposal.

My understanding is that Rust uses static multi-dispatch to do this. I don’t believe it has anything to do with structural sum types. Rust error handling uses a Result type with a single error case: http://doc.rust-lang.org/book/error-handling.html.

That example takes you through many of the options available. In the end, you end up at the sum-type for the error:
fn search<P: AsRef<Path>>
         (file_path: &Option<P>, city: &str)
         -> Result<Vec<PopulationCount>, CliError> {
    ...
}
It’s the CliError which is defined as:
enum CliError {
    Io(io::Error),
    Csv(csv::Error),
    NotFound,
}
The From() function essentially allows the try! macro to expand these in a nicer way.

So back to the proposal, one of the key things is to promote the `error` constant throughout the catch-clauses. This means that we can already leverage Swift’s pattern matching to solve this problem:

enum Combined {
    case IO(String)
    case Number(Int)
}

func simulate(err: Combined) {
    switch err {
    case let Combined.IO(string) where string == "hi": print("only hi!")
    case let Combined.IO(string): print(string)
    case let Combined.Number(value): print(value)
    }
}

simulate(Combined.IO("hi"))
simulate(Combined.IO("io"))
simulate(Combined.Number(9))

It’s not hard to use Swift’s pattern matching to extract out the inner information on an associated value enum and white the case/catch clauses. So unless I’m missing something, I think Swift already provides a good mechanism to do what you’re asking for, with the caveat that the `error` constant is promoted to be usable in the catch-clauses similar to how the switch-statements work.

Maybe adding this to the proposal would clarify usage?

How does this create a fragile API surface area? Adding a new error type to the signature would be a breaking change to the API contract. This is really no different than changing the type of error that can be thrown under your proposal.

It’s the same fragility that enums create; this was covered in the criticisms section. The likelihood of adding additional error cases is much greater than a change that would completely change the type of the error.

I see this functionality as a general limitation in the language. For example, errors are not the only context where you may want to return a type of A, B, or C. There have been other proposals on how we might do that in Swift. If and when it was solved in the general case for type parameters, I can’t foresee a compelling reason why it wouldn’t work in this context as well.

That makes sense in some ways, but I don’t think it’s unreasonable to ask for some analysis of whether a better design for typed errors would be possible if we had them. IMO it’s pretty important to get the design of typed errors right if / when we add them. If we don’t it will be considered a major mistake and will lead to a lot of less than desirable outcomes down the road.

I also think typed errors may be one of the more important use cases for structural sum types of some kind. If we are able to show that design problems that cannot be solved without them can be solved with them that might influence whether they are added or not. It might also influence when it makes sense to add support for typed errors to the language.

The problem can be solved without implicitly generated sum types though. The design of typed errors, as proposed, is to be consistent with the Swift type system today. Regardless, I’ve added a response in the “cirticisms” section that hopefully addresses this in some manner - basically, yes it would be helpful, but out of scope for this proposal.

That approach would make catch clauses rather clunky by nesting errors inside of associated values. If you’re advocating for this approach do you have any ideas on how to streamline syntax for catching them?

See above example. Does that address this concern?

-David

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


(Matthew Johnson) #17

Hi David,

I spent a lot of time last night thinking about how my concerns can be addressed without changes to the type system. I also spent some time working through some concrete examples this morning. This has helped narrow my concerns considerably.

I am going to suggest one addition to the proposal at the end. If you’re willing to incorporate that I will be pretty happy with what we can accomplish without any changes to the type system.

First, consider the case where there are some common errors which a library may throw in different places. These are considered to be part of the API contract. Some library functions may throw either common error depending on the code path taken.

Your proposal suggests we should fall back to throwing ErrorType in that case. This is not really a good solution in my mind.

A library should be able to have a family of error types it publishes in its API contract and some functions should be able to throw more than one. As you suggest, rather than a structural sum type we can manually create a sum type to do this.

I had two concerns about this. The first and most important was in the verbosity of catching the nested errors. Here’s an example:

enum CommonOne: ErrorType {
    case One
    case Two
}
enum CommonTwo:ErrorType {
    case One
    case Two
}
enum Both: ErrorType {
    case One(CommonOne)
    case Two(CommonTwo)
}

I was concerned that we would need to do something like this involving some verbose and nasty nesting, etc:

func functionThatThrowsBoth() throws Both { … }

do {
    try functionThatThrowsBoth()
}
catch .One(let inner) {
    switch inner {
        case .one: ...
        case .two: ...
    }
}
catch .Two(let inner) {
    switch inner {
        case .one: ...
        case .two: ...
    }
}

As it turns out, I am still getting familiar with the power of nested pattern matching and this was an unfounded concern. This is great! We can actually do this:

do {
    try functionThatThrowsBoth()
}
catch .One(.One) { ... }
catch .One(.Two) { ... }
catch .Two(.One) { ... }
catch .Two(.Two) { ... }

(note: this works today if you include a Both prefix in the cases which will be unnecessary with a typed error)

That is great! I have no concerns about this syntax for catching nested errors. This covers use cases that need to throw “multiple” error types pretty well. There are probably some edge cases where a structural sum type would be more convenient but I think they would be rare and am not concerned about them.

I would also like to comment that there are some interesting related ideas for enhancing enums in the "[Pitch] Use enums as enum underlying types” thread. They don’t directly impact the proposal but could make such use cases even more convenient if they are pursued independently.

The other concern I have is still valid, but I think a relatively straightforward solution is possible.

Continuing with the previous example, let’s look at the implementation of `functionThatThrowsBoth`:

func throwsInnerOne() throws InnerOne {
    throw InnerOne.One
}

func throwsInnerTwo() throws InnerTwo {
    throw InnerTwo.Two
}

func functionThatThrowsBoth(_ whichError: Bool) throws Both {
    do {
        if whichError {
            try throwsInnerOne()
        } else {
            try throwsInnerTwo()
        }
    }
    catch let inner as InnerOne { throw Both.One(inner) }
    catch let inner as InnerTwo { throw Both.Two(inner) }
}

The implementation is dominated by the concern of wrapping the error. This is pretty gross. This problem exists even if we are not wrapping the error, but rather translating the error from the underlying error type into the error type we are publishing in our API contract.

Here is an example where we are not wrapping the error, but translating it:

func functionThatCallsUnderlingyingThrows(_ whichError: Bool) throws MyPublishedError {
    do {
        try funcThatThrowsAnErrorThatMustBeTranslatedIntoMyPublishedError()
    }
    // catching logic that eventually throws MyPublishedErrorSomehow
}

The best we can do is to create a translation function or initializer:

enum MyPublishedError: ErrorType {
    init(_ error: UnderlyingError) { … }
}

func functionThatCallsUnderlingyingThrows(_ whichError: Bool) throws MyPublishedError {
    do {
        try funcThatThrowsAnErrorThatMustBeTranslatedIntoMyPublishedError()
    }
    catch let error as UnderlyingError { throw MyPublishedError(error) }
}

This is better as it removes the logic from the function itself. But it’s still not great as it introduces a lot of boilerplate everywhere we need to translate and / or wrap errors. The boilerplate also grows for each underlying error we need to translate:

func functionThatCallsUnderlingyingThrows(_ whichError: Bool) throws MyPublishedError {
    do {
        // bunch of stuff throwing several different errors
    }
    catch let error as UnderlyingError { throw MyPublishedError(error) }
    catch let error as OtherUnderlyingError { throw MyPublishedError(error) }
    // more catch clauses until we have covered every possible error type thrown by the body
    // hopefully the compiler wouldn’t require a default clause here but it probably would
}

This is the problem that `From` addresses in Rust. Swift is not Rust and our solution will look different. The point is that this is a problem and it can and has been solved.

My suggestion is that we should allow implicit conversion during error propagation. If the published error type has one and only one non-failable, non-throwing initializer that takes a single argument of the type that is thrown (including enum case initializers with a single associated value of the thrown type) that initializer is used to implicitly convert to the published error type. This conversion could be accomplished by synthesizing the necessary boilerplate or by some other means.

Now we have:

func functionThatCallsUnderlingyingThrows(_ whichError: Bool) throws MyPublishedError {
        try funcThatThrowsAnErrorThatMustBeTranslatedIntoMyPublishedError()
}

This looks as it should. We don’t pay a price of boilerplate for carefully designing the errors we expose in our API contract. This also handles automatic wrapping of errors where that is appropriate.

I don’t suggest implicit conversion lightly. I generally hate implicit conversions. But I think it makes a lot of sense here. It keeps concerns separate and removes boilerplate that distracts from the logic at hand, thus vastly improving readability. It is also likely to help minimize code impact when implementation details change and we need to modify how we are translating errors into the contract we expose.

If we don’t support the implicit conversion there are three paths that can be taken by developers. None of them are great and we will have three camps with different preference:

1. Just stick to untyped errors. I think there are some pretty smart people who think this will be a common practice even if we have support for typed errors in the language.
2. Allow underlying errors to flow through (when there is only a single underlying error type). This is brittle and I know you are of the opinion that it is a bad idea. I agree.
3. Write the boilerplate manually. This is annoying and is a significant barrier to clarity and readability.

I hope you will like the idea of implicit conversion during error propagation enough to add it to your proposal. With it I will be an enthusiastic supporter. It will help to establish good practices in the community for using typed errors in a robust and thoughtful way.

Without implicit error conversion I will still support the proposal but would plan to write a follow on proposal introducing the much needed (IMO) implicit conversion during error propagation. I would also expect opposition to the proposal during review from people concerned about one or more of the above listed options for dealing with error translation.

I think the idea of restricting typed errors to structs, enums, NSError, and final classes that has come up is a good one. It might be worth considering going further than that and restrict it to only enums and NSError. One of the biggest issues I have encountered with error handling during my career is that all too often the possible error cases are quite poorly documented. We have to allow NSError for Cocoa interop, but aside from the error types should really be enums IMO as they make it very clear what cases might need to be handled.

I want to thank you again for putting this proposal together and taking the time to consider and respond to feedback. Typed errors will be a significant step forward for Swift and I am looking forward to it.

Matthew

···

On Dec 18, 2015, at 12:36 PM, David Owens II <david@owensd.io> wrote:

On Dec 18, 2015, at 9:41 AM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

I’m not asking for you to speak for them. But I do think we need to learn from communities that are having success with typed error handling. Your proposal would be stronger if it went into detail about how it would avoid the problems that have been encountered in other languages. The experience of Rust could help to make that case as it is concrete and not hypothetical.

Sure, it could. It’s also anecdotal. It’s not necessarily true that something that works well in one context works well in another. It’s good to note that typed errors are wholly considered bad, but I’m not sure how much further we need to go then that. If you have specifics, then I could probably add them as an addendum to the proposal.

My understanding is that Rust uses static multi-dispatch to do this. I don’t believe it has anything to do with structural sum types. Rust error handling uses a Result type with a single error case: http://doc.rust-lang.org/book/error-handling.html.

That example takes you through many of the options available. In the end, you end up at the sum-type for the error:
fn search<P: AsRef<Path>>
         (file_path: &Option<P>, city: &str)
         -> Result<Vec<PopulationCount>, CliError> {
    ...
}
It’s the CliError which is defined as:
enum CliError {
    Io(io::Error),
    Csv(csv::Error),
    NotFound,
}
The From() function essentially allows the try! macro to expand these in a nicer way.

So back to the proposal, one of the key things is to promote the `error` constant throughout the catch-clauses. This means that we can already leverage Swift’s pattern matching to solve this problem:

enum Combined {
    case IO(String)
    case Number(Int)
}

func simulate(err: Combined) {
    switch err {
    case let Combined.IO(string) where string == "hi": print("only hi!")
    case let Combined.IO(string): print(string)
    case let Combined.Number(value): print(value)
    }
}

simulate(Combined.IO("hi"))
simulate(Combined.IO("io"))
simulate(Combined.Number(9))

It’s not hard to use Swift’s pattern matching to extract out the inner information on an associated value enum and white the case/catch clauses. So unless I’m missing something, I think Swift already provides a good mechanism to do what you’re asking for, with the caveat that the `error` constant is promoted to be usable in the catch-clauses similar to how the switch-statements work.

Maybe adding this to the proposal would clarify usage?

How does this create a fragile API surface area? Adding a new error type to the signature would be a breaking change to the API contract. This is really no different than changing the type of error that can be thrown under your proposal.

It’s the same fragility that enums create; this was covered in the criticisms section. The likelihood of adding additional error cases is much greater than a change that would completely change the type of the error.

I see this functionality as a general limitation in the language. For example, errors are not the only context where you may want to return a type of A, B, or C. There have been other proposals on how we might do that in Swift. If and when it was solved in the general case for type parameters, I can’t foresee a compelling reason why it wouldn’t work in this context as well.

That makes sense in some ways, but I don’t think it’s unreasonable to ask for some analysis of whether a better design for typed errors would be possible if we had them. IMO it’s pretty important to get the design of typed errors right if / when we add them. If we don’t it will be considered a major mistake and will lead to a lot of less than desirable outcomes down the road.

I also think typed errors may be one of the more important use cases for structural sum types of some kind. If we are able to show that design problems that cannot be solved without them can be solved with them that might influence whether they are added or not. It might also influence when it makes sense to add support for typed errors to the language.

The problem can be solved without implicitly generated sum types though. The design of typed errors, as proposed, is to be consistent with the Swift type system today. Regardless, I’ve added a response in the “cirticisms” section that hopefully addresses this in some manner - basically, yes it would be helpful, but out of scope for this proposal.

That approach would make catch clauses rather clunky by nesting errors inside of associated values. If you’re advocating for this approach do you have any ideas on how to streamline syntax for catching them?

See above example. Does that address this concern?

-David


(David Owens II) #18

I’d be ok with having enum/struct only error types, however, I don’t have a compelling reason to really limit their usage context. Also, that would complicate the bridging with NSError at the moment.

FYI: I’ve added some updates to the criticism section to qualify the Java checked-exceptions and the multiple error type annotations.

-David

···

On Dec 18, 2015, at 9:21 AM, Félix Cloutier <felixcca@yahoo.ca> wrote:

Oh, I see what you mean. I considered polymorphic types to be class hierarchies, when you're talking about ErrorType polymorphism.

Yes, I think that the compiler should be aware of what the function can throw, but I would be happier if it stayed a bit inconvenient to use reference types.

Le 18 déc. 2015 à 12:17:06, Félix Cloutier via swift-evolution <swift-evolution@swift.org> a écrit :

I'm not sure I understand your comment either. If I understand correctly, you say that the problem I describe is applicable only to polymorphic types (which is true). However, you then say that the only option today is polymorphic error types. Isn't that an issue? (Also, why is it the only option today?)

Le 18 déc. 2015 à 11:58:44, David Owens II <david@owensd.io> a écrit :

On Dec 18, 2015, at 7:03 AM, Félix Cloutier <felixcca@yahoo.ca> wrote:

For this reason, I don't like to encourage throwing polymorphic types, and I think that it's a misconception to pretend that having a single type in the throws annotation ensures that the function throws a single type. In my opinion, the fact that it's currently possible but awkward to use polymorphic types as errors is exactly as much support as the feature should receive.

I don’t follow this. Declaring a type that is an enum or a struct absolutely guarantees that the function only returns a single type. If the type is a class-based error, then sure, there’s not guarantee.

However, the only option today is polymorphic error types.

-David

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


(David Owens II) #19

Hi David

I started reading your proposal and I have a couple of questions.

In the Enum Base ErrorType example you mentioned that it requires a "catch { }" clause. However the code is already covering the two possible Enum values (OffBy1 and MutatedValue). Why is the "catch { }" required? I typed that code into a playground and I did not get any errors. Are you saying that because the Enum type could add a value in the future?

Playgrounds are basically in an anonymous function that throws, so the problem doesn’t show up there at the top level. Copy this into your playground.

enum MyError: ErrorType {
    case OnlyOne
}

func thrower() throws { throw MyError.OnlyOne }

func nolies() {
    do {
        try thrower()
    }
    catch MyError.OnlyOne { print("handled") }
    // catch { print("compiler error until uncommented") }
}

Also, you proposed the catch clause to use error as the name of the constant holding the error. Wouldn't it be better to let the programmer decide the name rather than hard coding it to use error? For example:

catch e where e.value == 0 { print("0") }
catch e where e.value == 1 { print("1") }
catch { print("nothing") }

The “error” name is already specified in the Swift rules for what the constant is. I don’t see any compelling reason to propose a change to that.

-David

···

On Dec 18, 2015, at 4:38 PM, Ricardo Parada <rparada@mac.com> wrote:


(Dennis Lysenko) #20

It certainly would complicate NSError bridging, David. What you could do is
treat it as a special case, so saying "throws NSError" in Swift would not
be allowed, but the objC functions that take an inout NSError parameter are
automatically annotated by "throws NSError" in Swift usage. This steps
around almost any restriction on what we programmers can specify to be
thrown in Swift.

···

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

I’d be ok with having enum/struct only error types, however, I don’t have
a compelling reason to really limit their usage context. Also, that would
complicate the bridging with NSError at the moment.

FYI: I’ve added some updates to the criticism section to qualify the Java
checked-exceptions and the multiple error type annotations.

-David

> On Dec 18, 2015, at 9:21 AM, Félix Cloutier <felixcca@yahoo.ca> wrote:
>
> Oh, I see what you mean. I considered polymorphic types to be class
hierarchies, when you're talking about ErrorType polymorphism.
>
> Yes, I think that the compiler should be aware of what the function can
throw, but I would be happier if it stayed a bit inconvenient to use
reference types.
>
>> Le 18 déc. 2015 à 12:17:06, Félix Cloutier via swift-evolution < > swift-evolution@swift.org> a écrit :
>>
>> I'm not sure I understand your comment either. If I understand
correctly, you say that the problem I describe is applicable only to
polymorphic types (which is true). However, you then say that the only
option today is polymorphic error types. Isn't that an issue? (Also, why is
it the only option today?)
>>
>>> Le 18 déc. 2015 à 11:58:44, David Owens II <david@owensd.io> a écrit :
>>>
>>>
>>>> On Dec 18, 2015, at 7:03 AM, Félix Cloutier <felixcca@yahoo.ca> > wrote:
>>>>
>>>> For this reason, I don't like to encourage throwing polymorphic
types, and I think that it's a misconception to pretend that having a
single type in the throws annotation ensures that the function throws a
single type. In my opinion, the fact that it's currently possible but
awkward to use polymorphic types as errors is exactly as much support as
the feature should receive.
>>>
>>> I don’t follow this. Declaring a type that is an enum or a struct
absolutely guarantees that the function only returns a single type. If the
type is a class-based error, then sure, there’s not guarantee.
>>>
>>> However, the only option today is polymorphic error types.
>>>
>>> -David
>>>
>>
>> _______________________________________________
>> 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