[Pitch] Typed throws

It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.

If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.

I don’t like any solution that is based on comments. I think that compiler should always ignore comments.

I agree. And in general, this sort of thing is exactly my core concern about adding typed throws to the language: I am completely certain that many programmers will add typed throws annotations because they're programmers and thus, well, probably a little obsessive/compulsive, and they're trying to precisely document the behavior of their function without necessarily thinking about the usefulness of that information for their clients and (if they're writing a library; and really you should almost always be writing code as if you're writing a library) whether they're actually willing to commit to that behavior in their interface. For those programmers, typed throws is just going to box them in and force them into anti-patterns in the long run.

In the vast majority of use-cases, clients are not going to exhaustively handle all errors — they will always have some generic fall-back. That is not pessimism, it's actually the natural result of the complicated world we live in, where code can fail for a huge host of reasons and most callers won't have meaningful special-case behavior for all of them. (On most operating systems, opening a file or a network connection can fail because you ran out of file descriptors. You're seriously telling me that you're going to add a special case to your error logic for that?) Go look at the actual error types that people use in most typed-throws situations and try to tell me I'm wrong — they probably have like twenty alternatives, and solidly a quarter or more of them will just be embedding some other arbitrarily-complex or stringly-typed error value.

The real use case for typed throws is when you have something like a parser library that really does only fail in a fixed number of semantically distinct ways, and you both (1) actually care about enforcing that in the implementation and making sure that other errors are handled internally and (2) you really do expect clients to exhaustively switch over the error at some point. That's important. But I continue to think that if adding better support for that use case misleads other programmers into thinking they should use typed throws, we will have made the language worse overall.

John.

···

On Feb 19, 2017, at 3:04 PM, Anton Zhilin via swift-evolution <swift-evolution@swift.org> wrote:

2017-02-18 18:27 GMT+03:00 Karl Wagner <razielim@gmail.com <mailto:razielim@gmail.com>>:

So, I’m not sure about what was decided last time, but my issues with this are:

- The thrown error type will become part of the ABI of the function. If you change the type of Error that is thrown, callers may not catch it. At the same time, if we make enums resilient by default and only allow specifying a single entire type, you will basically need one Error enum per function and it will need to be @fixed if you actually want to remove the catch-all block. Otherwise:

// Let’s say this isn’t @fixed...
enum CanFailError {
    errorOne
    errorTwo
}

func canFail() throws(CanFailError) { /* … */ }

do { try canFail() }
catch CanFailError {
    switch error {
        case .errorOne: /* handle error one */
        case .errorTwo: /* handle error two */
        default: /* handle possible new errors in later versions of the library */
    }
}

do { try canFail() }
catch .errorOne { /* handle error one */ }
catch .errorTwo { /* handle error two */ }
catch { /* handle possible new errors in later versions of the library */ }

- I usually have _semantic_ namespaces for Errors, rather than single types per implementation pattern. If we are adding strong annotations about which errors can be thrown, I’d quite like to incorporate that pattern. For example:

extension File {
@fixed enum OpeningError {
  case .invalidPath
  case .accessDenied // e.g. asking for write permissions for read-only file
}
@fixed enum ReadError {
  case .invalidOffset // past EOF
  case .deviceError // probably worth aborting the entire operation the read is part of
}

// - throws:
// - .OpeningError if the file can’t be opened
// - .ReadError if the read operation fails
func read(from offset: Int, into buffer: UnsafeBufferPointer<UInt8>) throws(OpeningError, ReadError) { /* … */ }
}

- I wonder if we could try something more ambitious. Since the list of thrown errors is resilience-breaking for the function, it is only beneficial for versioned and @inlineable functions. They should not be able to add new errors (they can remove them though, since errors are intended to be switched over). I wonder if we couldn’t introduce a small pattern grammar for our structured comments (isolated from the rest of the language) - it would be optional, but if you do list your errors, the compiler would validate that you do it exhaustively. Some patterns I would like are:

// - throws: - MyError.{errorOne, errorThree, errorFive}: Something bad || considered exhaustive
@inlineable public func canFail() throws {}

// - throws: - OpeningError: Computer says nooooo... || considered exhaustive if OpeningError is versioned or @fixed
// - * || other errors, requires “catch-all” by external callers
@inlineable public func canFail2() throws {}

If we want to get really clever, we can have the compiler automatically generate those error-lists for internal functions, so you would automatically get exhaustive error-handling within your own module.

- Karl

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

4 Likes

It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.

You’ve just re-invented the error handling model we have today.

If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential.

And now we’ve re-invented Java’s checked exceptions “throws Exception”.

To what end?

Russ

···

On Feb 19, 2017, at 12:04 PM, Anton Zhilin via swift-evolution <swift-evolution@swift.org> wrote:

Open enums can only add cases, not remove them. That means that new versions of a function will similarly only be able to add errors, and won’t be able to communicate that certain errors are no longer thrown. Protocols aren’t a solution, because you will need to write verbose conformances for all of your Error types.

Let me put this in another (perhaps more palatable) way. Forget comments, let’s say its part of some hidden metadata:

- The compiler lists every error which can be thrown by a function (it’s easily able to do that)
- Rather than making this part of the signature, the signature only says “throws an Error” and the actual Error list is written somewhere in the module as documentation/metadata.

Here’s why it’s so good:

- It’s completely free (you don’t write anything). The compiler generates everything for you.
- It’s **completely optional**: it won’t make your structure your Errors in a way that's less usable in a type-system sense for clients who don’t care about exhaustive catching.
- Exhaustive catching within your own module for free (you can omit a catch-all, the compiler won’t complain)

It’s good for resiliency, too:

- Non-resilient functions always reserve the right to throw new Errors in later versions. No system will get exhaustive catching for them anyway.
- If you stop throwing a specific Error, nothing breaks - it simply vanishes from the documentation/metadata. The compiler can simply warn about the redundant catch.
- Resilient (@versioned) functions can still offer exhaustive catching if we want to offer that. We might decide to make that opt-in/opt-out, because it would mean they would be limited to removing Errors, and never adding new ones.
—> As with any ABI promise, we will trap or it will be UB if you break the contract. We couldn’t validate it when compiling, but theoretically a validator could be built which compared two library versions.

- Karl

···

On 19 Feb 2017, at 21:04, Anton Zhilin <antonyzhilin@gmail.com> wrote:

It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.

If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.

I don’t like any solution that is based on comments. I think that compiler should always ignore comments.

1 Like

IMHO, there are two kinds of responses to errors - a specific response, and a general one. Only the calling code knows how it will deal with errors, so a “typed throws” is the function guessing possible calling code behavior.

The problem is, that gives four possible combinations - two where the function guesses correctly, and two where it doesn’t. The most damaging is when it suspects a caller doesn’t care about the error, when the caller actually does. This is unwanted wrapping.

To provide an example, imagine a library that parses JSON. It has several errors indicating JSON syntactic errors, and an “other” for representing errors on the input stream. It wraps the input stream errors so that it can provide a closed set of errors to the caller.

The caller is responsible for returning a data set. It doesn’t think that code calling ‘it” cares about JSON syntactic errors, merely that the object was not able to be restored. It returns its own wrapped error.

However, the original caller knows it is loading from disk. If the problem is due to an issue such as access permissions, It has to know implementation details of the API it called if it wishes to dive through the wrapped errors to find out if the problem was filesystem related.

Add more layers, and it can be very mysterious why a call failed. Java at least captures stack traces in this case to aid in technical support in diagnosing the error.

Wrapping exceptions also prevents an aggregate of errors from different subsystems being handled as a category, such as having a catch block handle RecoverableError generically

An interesting solution that has emerged in Ruby to keep library authors from wrapping exceptions is by decorating the existing exception. Exceptions are caught via pattern matching (same as in Swift), so rather than wrap an extension, they extend the error instance with a library-specific module (e.g. swift protocol). So while the error may be a IOError in ruby, you can still catch it via ‘rescue JSONError’

Trying to specify the exact errors becomes even more destructive with protocols and closures, where the person defining the interface knows neither which errors the implementor of the call will throw, nor necessarily if the caller will want to implement specific behavior on those errors. This in my personal Java coding experience almost always leads to wrapping in some protocol-specific Exception type which exposes minimal information to the caller, or exposing your errors in some unrelated type like IOException which was declared based on the author’s experience of possible exceptions.

-DW

···

On Feb 27, 2017, at 5:19 AM, Daniel Leping via swift-evolution <swift-evolution@swift.org> wrote:

On Mon, 27 Feb 2017 at 8:44 Dave Abrahams via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

on Fri Feb 17 2017, Joe Groff <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

> Experience in other languages like Rust and Haskell that use
> Result-based error propagation suggests that a single error type is
> adequate, and beneficial in many ways.

And experience in still others, like C++ and Java, suggests that
using static types to restrict the kind of information a function can
give you when an error occurs may actually be harmful.
+1 here. It becomes wrapping over wrapping over wrapping. Try doing a big app in Java (i.e. some kind of layered server) and you'll understand everything. Ones who tried and still want it - well, there are different tastes out there.

--
-Dave

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

OTOH, people *don't* seem to have these problems with Rust and functional languages with value-oriented error handling. This could be partly because there's a greater focus on fully-closed systems in those communities where resilience isn't a concern, and you can usually evolve all your use sites if you need to break an API, whereas C++ and Java projects are more likely to incorporate black-box components from multiple sources. Having affordances for unwinding with a well-typed error *within* a component seems like a generally useful thing; Haskell has do notation and Rust tosses macros at the problem to hide the propagation boilerplate, after all.

-Joe

···

On Feb 27, 2017, at 4:19 AM, Daniel Leping via swift-evolution <swift-evolution@swift.org> wrote:

On Mon, 27 Feb 2017 at 8:44 Dave Abrahams via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

on Fri Feb 17 2017, Joe Groff <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

> Experience in other languages like Rust and Haskell that use
> Result-based error propagation suggests that a single error type is
> adequate, and beneficial in many ways.

And experience in still others, like C++ and Java, suggests that
using static types to restrict the kind of information a function can
give you when an error occurs may actually be harmful.
+1 here. It becomes wrapping over wrapping over wrapping. Try doing a big app in Java (i.e. some kind of layered server) and you'll understand everything. Ones who tried and still want it - well, there are different tastes out there.

I'm sorry, I don't see any substantive difference, based on what you've
written here, between this feature and const.

Let me give it one more shot and then I’ll drop it. :)

Const is viral because if an API does not declare its arguments const
it cannot be used by a caller who has a const argument.

Unless the caller casts away const, thus erasing information that was
previously encoded in the type system.

It is required in order to make an API as generally useful as
possible.

Typed errors are not viral in this way because no callers are
prevented from calling an API regardless of whether it declares error
types or just throws Error like we have today.

Unless the caller can recover (which is *very* rare) or it catches and
rethrows one of the errors *it* declares, thus erasing information that
was previously encoded in the type system.

···

on Mon Feb 27 2017, Matthew Johnson <matthew-AT-anandabits.com> wrote:

On Feb 27, 2017, at 4:20 PM, Dave Abrahams <dabrahams@apple.com> wrote:

Pressure to declare error types in your signature in order to make
your function as generally useful as possible does not exist. Each
function is free to declare error types or not according to the
contract it wishes to expose.

An argument can be made that community expectations might develop that
good APIs should declare error types and they could be considered
viral in this sense because any API that is simply declared `throws`
is dropping type information. But I think this overstates the case.
The community appears to be very sensitive to the problems that can
arise when error types are too concrete, especially across module
boundaries. I think we can learn to use the tool where it works well
and to avoid it where it causes problems.

--
-Dave

--
-Dave

I'm sorry, I don't see any substantive difference, based on what you've
written here, between this feature and const.

Let me give it one more shot and then I’ll drop it. :)

Const is viral because if an API does not declare its arguments const
it cannot be used by a caller who has a const argument.

Unless the caller casts away const, thus erasing information that was
previously encoded in the type system.

Yes, of course.

It is required in order to make an API as generally useful as
possible.

Typed errors are not viral in this way because no callers are
prevented from calling an API regardless of whether it declares error
types or just throws Error like we have today.

Unless the caller can recover (which is *very* rare) or it catches and
rethrows one of the errors *it* declares, thus erasing information that
was previously encoded in the type system.

I view this as being fundamentally different than casting away const. Casting away const says “I know better than the types”.

Converting an error to a different type is extremely different. It much more similar to any other kind of value wrapper a library might create in order to shield its users from being coupled to its implementation details / dependencies. This is not a way *around* the type system in the sense that casting away const is. It is a way of *using* the type system (hopefully) to your advantage.

···

On Feb 27, 2017, at 5:01 PM, Dave Abrahams <dabrahams@apple.com> wrote:
on Mon Feb 27 2017, Matthew Johnson <matthew-AT-anandabits.com> wrote:

On Feb 27, 2017, at 4:20 PM, Dave Abrahams <dabrahams@apple.com> wrote:

Pressure to declare error types in your signature in order to make
your function as generally useful as possible does not exist. Each
function is free to declare error types or not according to the
contract it wishes to expose.

An argument can be made that community expectations might develop that
good APIs should declare error types and they could be considered
viral in this sense because any API that is simply declared `throws`
is dropping type information. But I think this overstates the case.
The community appears to be very sensitive to the problems that can
arise when error types are too concrete, especially across module
boundaries. I think we can learn to use the tool where it works well
and to avoid it where it causes problems.

--
-Dave

--
-Dave

I disagree with that, it works if you only have a single function parameter type that throws an error, but if there are more than one inferring the type won’t be possible anymore: func foo(_: () throws(T) -> Void, _: () throws(S) -> Void) rethrows(S) (here, we’re assuming that T is handled inside foo).

Well, isn't it actually incorrect that "rethrows" is a property of the function, and not of its parameters?

Just imagine this

func catches(catchMe: () throws -> Void, throwMe: () throws -> Void) rethrows {
  try? catchMe()
  try throwMe()
  print("Nothing thrown")
}

func doesThrow() throws {
  throw NSError()
}

func doesntThrow() {
  print("I'm a nice function")
}

try catches(catchMe: doesThrow, throwMe: doesntThrow)

The last call can't throw, but the compiler still needs the "try" on it.
I guess in real-world code, this isn't an issue — but still, it's wrong… so if we want a really sophisticated model for error handling, imho this should be addressed.

Maybe we should also take into account that a higher-order function might catch some errors, and only rethrow for special cases, and this adds complexity as well?
In this case, each function-parameter would require a full list a errors that are filtered out, or even a translation-table… I guess most developers would agree that this is overkill, and that keeping things simple has merit as well.

This is something I have discussed extensively in the thread titled “Analysis of the design of typed throws”. I am interested if you have any comments on that discussion.

···

On Feb 28, 2017, at 12:27 PM, Tino Heth via swift-evolution <swift-evolution@swift.org> wrote:

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

I agree that overly specific API specs are a concern. There's still some documentational benefit to a typed error enum, even if it ultimately ends up containing a catch-all case, since it gives you the ability to also enumerate some number of discrete, realistically handleable cases. Sure, your network request running in an XPC service has infinitely many failure cases due to resource constraints or IPC failure or network chaos, but there's usually some number of domain-specific error cases too. And if we compare the proposed "you get to specify one error enum type" model to, say, Java or C++98's "you get to specify any number of error classes" model, I think that helps steer people away from the most grievous API mistakes in Java land, since instead of listing a closed set of concrete error classes directly in your API signature, you'll list those error cases in your enum definition, and in a resilient world, the enum will be "open" by default to external users, preventing it from being a permanent liability unless the user explicitly opted into closedness.

In the discussions around Rust's error handling conventions, they recognized this pattern of APIs either raising some number of layer-appropriate errors or carrying forward the failure modes of the layers below them, and they developed a convention for errors wrapping other errors. It would be reasonable to say we should do the same thing as part of the typed errors design. If we were to generalize enum subtyping beyond Optional, that might be one way to go about it, letting an enum wrap its underlying layers' failure cases as subtypes.

-Joe

···

On Feb 20, 2017, at 9:57 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 19, 2017, at 3:04 PM, Anton Zhilin via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.

If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.

I don’t like any solution that is based on comments. I think that compiler should always ignore comments.

I agree. And in general, this sort of thing is exactly my core concern about adding typed throws to the language: I am completely certain that many programmers will add typed throws annotations because they're programmers and thus, well, probably a little obsessive/compulsive, and they're trying to precisely document the behavior of their function without necessarily thinking about the usefulness of that information for their clients and (if they're writing a library; and really you should almost always be writing code as if you're writing a library) whether they're actually willing to commit to that behavior in their interface. For those programmers, typed throws is just going to box them in and force them into anti-patterns in the long run.

In the vast majority of use-cases, clients are not going to exhaustively handle all errors — they will always have some generic fall-back. That is not pessimism, it's actually the natural result of the complicated world we live in, where code can fail for a huge host of reasons and most callers won't have meaningful special-case behavior for all of them. (On most operating systems, opening a file or a network connection can fail because you ran out of file descriptors. You're seriously telling me that you're going to add a special case to your error logic for that?) Go look at the actual error types that people use in most typed-throws situations and try to tell me I'm wrong — they probably have like twenty alternatives, and solidly a quarter or more of them will just be embedding some other arbitrarily-complex or stringly-typed error value.

I agree, and that’s where I was going with it: I think that typed-throws should basically be something on the level of a stronger comment rather than something so definitive as the function’s ABI. That’s how it will be much of the time in practice, anyway.

I don’t believe having a single error type is really ideal for anything. We’ve basically whittled down the feature until it gets in the way. If every function is throwing its own enum or hidden under complex hierarchies of protocols, it becomes difficult to write helper routines which respond to common errors in certain ways (e.g. trying an operation if if failed because the network was down).

// Using one-enum per function

enum FunctionOneError: Error {
    case networkWasDown(shouldTryAgain: Bool)
    case otherReason
}
func functionOne() throws(FunctionOneError)

enum FunctionTwoError: Error {
    case networkWasDown(shouldTryAgain: Bool)
    case aDifferentReason
}
func functionTwo() throws(FunctionTwoError)

// How to use this information at a high level?

func retryIfNetworkDown(let attempts: Int = 3, work: ()throws->Void) rethrows -> Bool { // <- Can’t specify which errors we take, or which we rethrow
    for n in 0..<attempts {
        do { try work() }
        catch FunctionOneError.networkWasDown(let tryAgain) {
            if tryAgain, n<attempts { continue }
            else { return false }
        }
        catch FunctionTwoError.networkWasDown(let tryAgain) { // Needs to handle per-function errors :(
            if tryAgain, n<attempts { continue }
            else { return false }
        }
        catch { throw error }
    }
}

So I’ve heard people say you should create a protocol then, but that’s not really a convenient solution either...

protocol NetworkError {
    func wasNetworkDownAndShouldTryAgain() -> (Bool, Bool)
}

extension FunctionOneError: NetworkError {
    func wasNetworkDownAndShouldTryAgain() -> (Bool, Bool) {
        guard case .networkWasDown(let tryAgain) = self else { return (false, false) }
        return (true, tryAgain)
    }
}

// This needs to be done twice, too...

extension FunctionTwoError: NetworkError {
    func wasNetworkDownAndShouldTryAgain() -> (Bool, Bool) {
        guard case .networkWasDown(let tryAgain) = self else { return (false, false) }
        return (true, tryAgain)
    }
}

So I think it all descends in to lots of syntax for very marginal amounts of value. Typed-throws is never likely to be the wondrous 100% cross-library reliability guarantee that people dream of. I view it more like good documentation.

- Karl

···

On 20 Feb 2017, at 18:57, John McCall <rjmccall@apple.com> wrote:

On Feb 19, 2017, at 3:04 PM, Anton Zhilin via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.

If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.

I don’t like any solution that is based on comments. I think that compiler should always ignore comments.

I agree. And in general, this sort of thing is exactly my core concern about adding typed throws to the language: I am completely certain that many programmers will add typed throws annotations because they're programmers and thus, well, probably a little obsessive/compulsive, and they're trying to precisely document the behavior of their function without necessarily thinking about the usefulness of that information for their clients and (if they're writing a library; and really you should almost always be writing code as if you're writing a library) whether they're actually willing to commit to that behavior in their interface. For those programmers, typed throws is just going to box them in and force them into anti-patterns in the long run.

In the vast majority of use-cases, clients are not going to exhaustively handle all errors — they will always have some generic fall-back. That is not pessimism, it's actually the natural result of the complicated world we live in, where code can fail for a huge host of reasons and most callers won't have meaningful special-case behavior for all of them. (On most operating systems, opening a file or a network connection can fail because you ran out of file descriptors. You're seriously telling me that you're going to add a special case to your error logic for that?) Go look at the actual error types that people use in most typed-throws situations and try to tell me I'm wrong — they probably have like twenty alternatives, and solidly a quarter or more of them will just be embedding some other arbitrarily-complex or stringly-typed error value.

The real use case for typed throws is when you have something like a parser library that really does only fail in a fixed number of semantically distinct ways, and you both (1) actually care about enforcing that in the implementation and making sure that other errors are handled internally and (2) you really do expect clients to exhaustively switch over the error at some point. That's important. But I continue to think that if adding better support for that use case misleads other programmers into thinking they should use typed throws, we will have made the language worse overall.

John.

2017-02-18 18:27 GMT+03:00 Karl Wagner <razielim@gmail.com <mailto:razielim@gmail.com>>:

So, I’m not sure about what was decided last time, but my issues with this are:

- The thrown error type will become part of the ABI of the function. If you change the type of Error that is thrown, callers may not catch it. At the same time, if we make enums resilient by default and only allow specifying a single entire type, you will basically need one Error enum per function and it will need to be @fixed if you actually want to remove the catch-all block. Otherwise:

// Let’s say this isn’t @fixed...
enum CanFailError {
    errorOne
    errorTwo
}

func canFail() throws(CanFailError) { /* … */ }

do { try canFail() }
catch CanFailError {
    switch error {
        case .errorOne: /* handle error one */
        case .errorTwo: /* handle error two */
        default: /* handle possible new errors in later versions of the library */
    }
}

do { try canFail() }
catch .errorOne { /* handle error one */ }
catch .errorTwo { /* handle error two */ }
catch { /* handle possible new errors in later versions of the library */ }

- I usually have _semantic_ namespaces for Errors, rather than single types per implementation pattern. If we are adding strong annotations about which errors can be thrown, I’d quite like to incorporate that pattern. For example:

extension File {
@fixed enum OpeningError {
  case .invalidPath
  case .accessDenied // e.g. asking for write permissions for read-only file
}
@fixed enum ReadError {
  case .invalidOffset // past EOF
  case .deviceError // probably worth aborting the entire operation the read is part of
}

// - throws:
// - .OpeningError if the file can’t be opened
// - .ReadError if the read operation fails
func read(from offset: Int, into buffer: UnsafeBufferPointer<UInt8>) throws(OpeningError, ReadError) { /* … */ }
}

- I wonder if we could try something more ambitious. Since the list of thrown errors is resilience-breaking for the function, it is only beneficial for versioned and @inlineable functions. They should not be able to add new errors (they can remove them though, since errors are intended to be switched over). I wonder if we couldn’t introduce a small pattern grammar for our structured comments (isolated from the rest of the language) - it would be optional, but if you do list your errors, the compiler would validate that you do it exhaustively. Some patterns I would like are:

// - throws: - MyError.{errorOne, errorThree, errorFive}: Something bad || considered exhaustive
@inlineable public func canFail() throws {}

// - throws: - OpeningError: Computer says nooooo... || considered exhaustive if OpeningError is versioned or @fixed
// - * || other errors, requires “catch-all” by external callers
@inlineable public func canFail2() throws {}

If we want to get really clever, we can have the compiler automatically generate those error-lists for internal functions, so you would automatically get exhaustive error-handling within your own module.

- Karl

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

So here’s my counter-proposal, fleshed out:

Specially, on resiliency:

## Internally to a module

Compiler can use generated Error-list metadata to:
- provide compile-errors with specific uncaught errors (better diagnostics)
- allow omitting catch-alls
- optimise away Error existential allocations

All of that would automatically apply to all throwing/rethrowing functions, without any additional developer effort.

## Cross-module

Compiler can use generated Error-list metadata to:
- Inform users about errors that might get thrown by this version of the function (purely documentation)
- Allow omitting catch-alls for specific functions which opt-in to that contract.

And that’s it. Notice there is no behavioural change; the Error-list metadata is entirely optional.

### Exhaustive catching cross-module

Resilient functions can _additionally_ promise to never throw new Errors. It should be an additional promise. From an error-list perspective, the function makes an additional promise that later versions of the error-list will never get more inclusive.

We can’t check that resilience at compile-time, though. If you change the function signature, you will get a error in the dynamic linker. Similarly, if somebody just adds a case to their @fixed enum, you won’t know until runtime when it gets thrown and nobody’s there to catch it.

- It would be cool if we failed gracefully in that case; if the caller wasn’t catching exhaustively, the new error should fall in to the existing catch-all.
- Otherwise, if the caller was assuming the library author kept their promise and omitted a catch-all, we should still synthesise one to provide a unique trap location (something like swift_resilience_unexpectedError()).
    - It means we can't optimise away the Error existential cross-module (maybe in -Ounchecked?), but that seems to me like an acceptable cost.

The big problem with this is that it relies on unwritten, generated metadata. For resilient functions promising resilient error-lists, it’s helpful to have the errors you’re promising written down and easily manually inspectable. That’s why I initially suggested having the compiler validate the documentation comments. We could still do that - so if you have a @versioned function and you additionally say that the errors it throws are also @versioned, you have to write a comment listing every Error (and the compiler will check it).

Small example:

enum FailureReason {
    case deviceBusy
    case networkDown
    case notFound
}

//% [Error-list]: FailureReason.notFound
func openFile(_ path: String) throws { … }

//% @versioned [Error-list]: FailureReason.notFound, FailureReason.deviceBusy
@versioned(includingErrors)
func read(_ path: String, range: Range<Int>) -> Data { … }

//% [Error-list]: <rethrows from arg1>
func retrying(attempts n: Int, _ work: ()throws -> Void) rethrows -> Bool {
  for _ in 0..<attempts {
    do { try work(); return true }
    catch FailureReason.deviceBusy { continue }
    catch FailureReason.networkDown { continue }
    catch { throw error }
  }
  return false
}

//% [Error-list]: <rethrows from arg0, masks: FailureReason.notFound>
func mustBeFound(_ work: ()throws -> Void) rethrows {
    do { try work() }
    catch FailureReason.notFound { fatalError(“This thing must be found") }
    catch { throw error }
}

mustBeFound { openFile(“test.txt”) } // can be proven not to throw (in same module), because mustBeFound handles all errors

// cross-module

// Function does not have a versioned error-list; catch-all is mandatory.

do { try openFile(“test.txt”) }
catch .notFound { print(“not found!”) }
catch { print(“other error: \(error}”) }

do { try mustBeFound { open(“test.txt”) } }
catch { print(“other error: \(error}”) }

// Function has a versioned error-list; catch-all is optional.

do { try readFile(“test.txt”, range: 0..<64) }
catch .notFound { print(“file not found!”) }
catch .deviceBusy { /* maybe retry? */ }
// [implicit] catch { _swift_reslience_unexpectedError() }

···

On 21 Feb 2017, at 00:34, Karl Wagner <karl.swift@springsup.com> wrote:

On 19 Feb 2017, at 21:04, Anton Zhilin <antonyzhilin@gmail.com <mailto:antonyzhilin@gmail.com>> wrote:

It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.

If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.

I don’t like any solution that is based on comments. I think that compiler should always ignore comments.

Open enums can only add cases, not remove them. That means that new versions of a function will similarly only be able to add errors, and won’t be able to communicate that certain errors are no longer thrown. Protocols aren’t a solution, because you will need to write verbose conformances for all of your Error types.

Let me put this in another (perhaps more palatable) way. Forget comments, let’s say its part of some hidden metadata:

- The compiler lists every error which can be thrown by a function (it’s easily able to do that)
- Rather than making this part of the signature, the signature only says “throws an Error” and the actual Error list is written somewhere in the module as documentation/metadata.

Here’s why it’s so good:

- It’s completely free (you don’t write anything). The compiler generates everything for you.
- It’s **completely optional**: it won’t make your structure your Errors in a way that's less usable in a type-system sense for clients who don’t care about exhaustive catching.
- Exhaustive catching within your own module for free (you can omit a catch-all, the compiler won’t complain)

It’s good for resiliency, too:

- Non-resilient functions always reserve the right to throw new Errors in later versions. No system will get exhaustive catching for them anyway.
- If you stop throwing a specific Error, nothing breaks - it simply vanishes from the documentation/metadata. The compiler can simply warn about the redundant catch.
- Resilient (@versioned) functions can still offer exhaustive catching if we want to offer that. We might decide to make that opt-in/opt-out, because it would mean they would be limited to removing Errors, and never adding new ones.
—> As with any ABI promise, we will trap or it will be UB if you break the contract. We couldn’t validate it when compiling, but theoretically a validator could be built which compared two library versions.

- Karl

IMHO, there are two kinds of responses to errors - a specific response, and a general one. Only the calling code knows how it will deal with errors, so a “typed throws” is the function guessing possible calling code behavior.

The problem is, that gives four possible combinations - two where the function guesses correctly, and two where it doesn’t. The most damaging is when it suspects a caller doesn’t care about the error, when the caller actually does. This is unwanted wrapping.

To provide an example, imagine a library that parses JSON. It has several errors indicating JSON syntactic errors, and an “other” for representing errors on the input stream. It wraps the input stream errors so that it can provide a closed set of errors to the caller.

The caller is responsible for returning a data set. It doesn’t think that code calling ‘it” cares about JSON syntactic errors, merely that the object was not able to be restored. It returns its own wrapped error.

However, the original caller knows it is loading from disk. If the problem is due to an issue such as access permissions, It has to know implementation details of the API it called if it wishes to dive through the wrapped errors to find out if the problem was filesystem related.

On the other hand, without wrapping the caller still needs to know the implementation details of the API in order to know what errors its dependencies might throw and the burden of grouping errors into higher-level categories is punted to the caller. Some callers might appreciate the directness and specificity of the original error but many others might appreciate the higher level grouping.

This is ultimately a question of API design where there is no one right answer.

Add more layers, and it can be very mysterious why a call failed. Java at least captures stack traces in this case to aid in technical support in diagnosing the error.

Wrapping exceptions also prevents an aggregate of errors from different subsystems being handled as a category, such as having a catch block handle RecoverableError generically

An interesting solution that has emerged in Ruby to keep library authors from wrapping exceptions is by decorating the existing exception. Exceptions are caught via pattern matching (same as in Swift), so rather than wrap an extension, they extend the error instance with a library-specific module (e.g. swift protocol). So while the error may be a IOError in ruby, you can still catch it via ‘rescue JSONError’

If I understand this correctly it sounds like introducing a library would create protocols to categorize errors and add retroactive conformances to these protocols for errors thrown by its dependencies? That is an interesting approach. But it requires knowing the concrete types of the possible errors all the way down the stack (you can’t add a conformance to an existential). This seems very problematic to me, especially in a language where creating new error types is as easy as it is in Swift.

Error handling is messy, there’s no doubt about that. I would like to have as many tools at my disposal as possible. Error types is one of those tools.

···

On Feb 27, 2017, at 1:46 PM, David Waite via swift-evolution <swift-evolution@swift.org> wrote:

Trying to specify the exact errors becomes even more destructive with protocols and closures, where the person defining the interface knows neither which errors the implementor of the call will throw, nor necessarily if the caller will want to implement specific behavior on those errors. This in my personal Java coding experience almost always leads to wrapping in some protocol-specific Exception type which exposes minimal information to the caller, or exposing your errors in some unrelated type like IOException which was declared based on the author’s experience of possible exceptions.

-DW

On Feb 27, 2017, at 5:19 AM, Daniel Leping via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Mon, 27 Feb 2017 at 8:44 Dave Abrahams via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

on Fri Feb 17 2017, Joe Groff <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

> Experience in other languages like Rust and Haskell that use
> Result-based error propagation suggests that a single error type is
> adequate, and beneficial in many ways.

And experience in still others, like C++ and Java, suggests that
using static types to restrict the kind of information a function can
give you when an error occurs may actually be harmful.
+1 here. It becomes wrapping over wrapping over wrapping. Try doing a big app in Java (i.e. some kind of layered server) and you'll understand everything. Ones who tried and still want it - well, there are different tastes out there.

--
-Dave

_______________________________________________
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

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

IMHO, there are two kinds of responses to errors - a specific response, and a general one. Only the calling code knows how it will deal with errors, so a “typed throws” is the function guessing possible calling code behavior.

The problem is, that gives four possible combinations - two where the function guesses correctly, and two where it doesn’t. The most damaging is when it suspects a caller doesn’t care about the error, when the caller actually does. This is unwanted wrapping.

To provide an example, imagine a library that parses JSON. It has several errors indicating JSON syntactic errors, and an “other” for representing errors on the input stream. It wraps the input stream errors so that it can provide a closed set of errors to the caller.

The caller is responsible for returning a data set. It doesn’t think that code calling ‘it” cares about JSON syntactic errors, merely that the object was not able to be restored. It returns its own wrapped error.

However, the original caller knows it is loading from disk. If the problem is due to an issue such as access permissions, It has to know implementation details of the API it called if it wishes to dive through the wrapped errors to find out if the problem was filesystem related.

Add more layers, and it can be very mysterious why a call failed. Java at least captures stack traces in this case to aid in technical support in diagnosing the error.

Wrapping exceptions also prevents an aggregate of errors from different subsystems being handled as a category, such as having a catch block handle RecoverableError generically

Fwiw, I think wrapping errors is something that people are sometimes going to want to do regardless of whether they are typed or not. Maybe the solution is to better support wrapping errors by focusing on the problems that wrapping causes. For example, we could do something like this to make it easier to get at the original error:

protocol Error {
  // The error directly underlying this error.
  // Ideally the compiler would synthesize an implementation for enums conforming to `Error`
  // If `self` is a case that has an associate value which is or conforms to `Error` that error would be returned, otherwise `nil` would be returned.
  var underlyingError: Error? { get }

  // The original error underlying *all* layers of wrapping.
  // If underlyingError is non-nil this is also non-nil.
  var originalError: Error { get }
}
extension Error {
    var underlyingError: Error? {
      return nil
    }
    var originalError: Error {
      return underlyingError?.originalError ?? underlyingError ?? self
    }
}

We could even provide syntactic sugar for catch sites that want to deal with the original error rather than the wrapped error if that is an important use case.

···

On Feb 27, 2017, at 1:46 PM, David Waite via swift-evolution <swift-evolution@swift.org> wrote:

An interesting solution that has emerged in Ruby to keep library authors from wrapping exceptions is by decorating the existing exception. Exceptions are caught via pattern matching (same as in Swift), so rather than wrap an extension, they extend the error instance with a library-specific module (e.g. swift protocol). So while the error may be a IOError in ruby, you can still catch it via ‘rescue JSONError’

Trying to specify the exact errors becomes even more destructive with protocols and closures, where the person defining the interface knows neither which errors the implementor of the call will throw, nor necessarily if the caller will want to implement specific behavior on those errors. This in my personal Java coding experience almost always leads to wrapping in some protocol-specific Exception type which exposes minimal information to the caller, or exposing your errors in some unrelated type like IOException which was declared based on the author’s experience of possible exceptions.

-DW

On Feb 27, 2017, at 5:19 AM, Daniel Leping via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Mon, 27 Feb 2017 at 8:44 Dave Abrahams via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

on Fri Feb 17 2017, Joe Groff <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

> Experience in other languages like Rust and Haskell that use
> Result-based error propagation suggests that a single error type is
> adequate, and beneficial in many ways.

And experience in still others, like C++ and Java, suggests that
using static types to restrict the kind of information a function can
give you when an error occurs may actually be harmful.
+1 here. It becomes wrapping over wrapping over wrapping. Try doing a big app in Java (i.e. some kind of layered server) and you'll understand everything. Ones who tried and still want it - well, there are different tastes out there.

--
-Dave

_______________________________________________
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

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

> Experience in other languages like Rust and Haskell that use
> Result-based error propagation suggests that a single error type is
> adequate, and beneficial in many ways.

And experience in still others, like C++ and Java, suggests that
using static types to restrict the kind of information a function can
give you when an error occurs may actually be harmful.
+1 here. It becomes wrapping over wrapping over wrapping. Try doing a big app in Java (i.e. some kind of layered server) and you'll understand everything. Ones who tried and still want it - well, there are different tastes out there.

OTOH, people *don't* seem to have these problems with Rust and functional languages with value-oriented error handling. This could be partly because there's a greater focus on fully-closed systems in those communities where resilience isn't a concern, and you can usually evolve all your use sites if you need to break an API, whereas C++ and Java projects are more likely to incorporate black-box components from multiple sources. Having affordances for unwinding with a well-typed error *within* a component seems like a generally useful thing; Haskell has do notation and Rust tosses macros at the problem to hide the propagation boilerplate, after all.

+1. There are places where typed errors are very useful and places where they can cause problems (as with many features a programming language can have).

···

On Feb 27, 2017, at 3:34 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 27, 2017, at 4:19 AM, Daniel Leping via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
On Mon, 27 Feb 2017 at 8:44 Dave Abrahams via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
on Fri Feb 17 2017, Joe Groff <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

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

David, IMHO, all you say is absolutely true and typed throws might work
well, but in theoretic idealistic world. In reality though, you end having
more exception types than data types, feature cost rises exponentially and
the code becomes cluttered with all the wrapping.

I seriously don't understand why would one even think of this feature after
it's proven a bad practice by Java community. Even Java based languages
(i.e. Scala) have dropped this "feature".

···

On Mon, 27 Feb 2017 at 21:46 David Waite <david@alkaline-solutions.com> wrote:

IMHO, there are two kinds of responses to errors - a specific response,
and a general one. Only the calling code knows how it will deal with
errors, so a “typed throws” is the function guessing possible calling code
behavior.

The problem is, that gives four possible combinations - two where the
function guesses correctly, and two where it doesn’t. The most damaging is
when it suspects a caller doesn’t care about the error, when the caller
actually does. This is unwanted wrapping.

To provide an example, imagine a library that parses JSON. It has several
errors indicating JSON syntactic errors, and an “other” for representing
errors on the input stream. It wraps the input stream errors so that it can
provide a closed set of errors to the caller.

The caller is responsible for returning a data set. It doesn’t think that
code calling ‘it” cares about JSON syntactic errors, merely that the object
was not able to be restored. It returns its own wrapped error.

However, the original caller knows it is loading from disk. If the problem
is due to an issue such as access permissions, It has to know
implementation details of the API it called if it wishes to dive through
the wrapped errors to find out if the problem was filesystem related.

Add more layers, and it can be very mysterious why a call failed. Java at
least captures stack traces in this case to aid in technical support in
diagnosing the error.

Wrapping exceptions also prevents an aggregate of errors from different
subsystems being handled as a category, such as having a catch block handle
RecoverableError generically

An interesting solution that has emerged in Ruby to keep library authors
from wrapping exceptions is by decorating the existing exception.
Exceptions are caught via pattern matching (same as in Swift), so rather
than wrap an extension, they extend the error instance with a
library-specific module (e.g. swift protocol). So while the error may be a
IOError in ruby, you can still catch it via ‘rescue JSONError’

Trying to specify the exact errors becomes even more destructive with
protocols and closures, where the person defining the interface knows
neither which errors the implementor of the call will throw, nor
necessarily if the caller will want to implement specific behavior on those
errors. This in my personal Java coding experience almost always leads to
wrapping in some protocol-specific Exception type which exposes minimal
information to the caller, or exposing your errors in some unrelated type
like IOException which was declared based on the author’s experience of
possible exceptions.

-DW

On Feb 27, 2017, at 5:19 AM, Daniel Leping via swift-evolution < > swift-evolution@swift.org> wrote:

On Mon, 27 Feb 2017 at 8:44 Dave Abrahams via swift-evolution < > swift-evolution@swift.org> wrote:

on Fri Feb 17 2017, Joe Groff <swift-evolution@swift.org> wrote:

> Experience in other languages like Rust and Haskell that use
> Result-based error propagation suggests that a single error type is
> adequate, and beneficial in many ways.

And experience in still others, like C++ and Java, suggests that
using static types to restrict the kind of information a function can
give you when an error occurs may actually be harmful.

+1 here. It becomes wrapping over wrapping over wrapping. Try doing a big
app in Java (i.e. some kind of layered server) and you'll understand
everything. Ones who tried and still want it - well, there are different
tastes out there.

--
-Dave

_______________________________________________
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

Well, as Dave pointed, you can very rarely recover from an error, which IMO
is absolutely true.

If your operation fails you don't really care unless you can recover. And
you know your cases, which you can recover from (in reality one usually
does it in optimization phase, though).

What else do you need the type of error for at the very end of your call
stack? In 90% you will tell the user "ah, sorry, something happened. Come
back later" and log the error (if you haven't forgot it).

In most cases the errors are not for recovering. They neither are to be
presented to users. They are for developers to read the log/crash
report/whatever else and analyze it. Most of the errors are for debugging
purposes.

I don't want to deal with cumbersome code the purpose of which is to just
"obey the language rules". Unless I know how to recover I would rethrow it.
Than catch at the top of the stack and log + show the user a nice "sorry"
message without getting techy.

···

On Tue, 28 Feb 2017 at 1:12 Matthew Johnson via swift-evolution < swift-evolution@swift.org> wrote:

> On Feb 27, 2017, at 5:01 PM, Dave Abrahams <dabrahams@apple.com> wrote:
>
>
> on Mon Feb 27 2017, Matthew Johnson <matthew-AT-anandabits.com> wrote:
>
>>> On Feb 27, 2017, at 4:20 PM, Dave Abrahams <dabrahams@apple.com> > wrote:
>>>
>>>
>>> I'm sorry, I don't see any substantive difference, based on what you've
>>> written here, between this feature and const.
>>
>> Let me give it one more shot and then I’ll drop it. :)
>>
>> Const is viral because if an API does not declare its arguments const
>> it cannot be used by a caller who has a const argument.
>
> Unless the caller casts away const, thus erasing information that was
> previously encoded in the type system.

Yes, of course.

>
>> It is required in order to make an API as generally useful as
>> possible.
>>
>> Typed errors are not viral in this way because no callers are
>> prevented from calling an API regardless of whether it declares error
>> types or just throws Error like we have today.
>
> Unless the caller can recover (which is *very* rare) or it catches and
> rethrows one of the errors *it* declares, thus erasing information that
> was previously encoded in the type system.

I view this as being fundamentally different than casting away const.
Casting away const says “I know better than the types”.

Converting an error to a different type is extremely different. It much
more similar to any other kind of value wrapper a library might create in
order to shield its users from being coupled to its implementation details
/ dependencies. This is not a way *around* the type system in the sense
that casting away const is. It is a way of *using* the type system
(hopefully) to your advantage.

>
>> Pressure to declare error types in your signature in order to make
>> your function as generally useful as possible does not exist. Each
>> function is free to declare error types or not according to the
>> contract it wishes to expose.
>>
>> An argument can be made that community expectations might develop that
>> good APIs should declare error types and they could be considered
>> viral in this sense because any API that is simply declared `throws`
>> is dropping type information. But I think this overstates the case.
>> The community appears to be very sensitive to the problems that can
>> arise when error types are too concrete, especially across module
>> boundaries. I think we can learn to use the tool where it works well
>> and to avoid it where it causes problems.
>>
>>>
>>> --
>>> -Dave
>>
>
> --
> -Dave

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

As you know, I still think that adding typed throws is the right thing to do. I understand your concern about “the feature could be misused” but the same thing is true about many other language features.

One thing you didn’t mention is that boxing thrown values in an existential requires allocation in the general case. This may be unacceptable for some classes of Swift application (in the embedded / deep systems space) or simply undesirable because of the performance implication.

-Chris

···

On Feb 20, 2017, at 9:57 AM, John McCall via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 19, 2017, at 3:04 PM, Anton Zhilin via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.

If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.

I don’t like any solution that is based on comments. I think that compiler should always ignore comments.

I agree. And in general, this sort of thing is exactly my core concern about adding typed throws to the language: I am completely certain that many programmers will add typed throws annotations because they're programmers and thus, well, probably a little obsessive/compulsive, and they're trying to precisely document the behavior of their function without necessarily thinking about the usefulness of that information for their clients and (if they're writing a library; and really you should almost always be writing code as if you're writing a library) whether they're actually willing to commit to that behavior in their interface. For those programmers, typed throws is just going to box them in and force them into anti-patterns in the long run.

I completely agree that what most programmers are looking for is just a blessed way to document that a function is likely to throw specific kinds of error, and that certain of them might be worth considering in the caller.

John.

···

On Feb 20, 2017, at 3:46 PM, Karl Wagner <razielim@gmail.com> wrote:

On 20 Feb 2017, at 18:57, John McCall <rjmccall@apple.com <mailto:rjmccall@apple.com>> wrote:

On Feb 19, 2017, at 3:04 PM, Anton Zhilin via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.

If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.

I don’t like any solution that is based on comments. I think that compiler should always ignore comments.

I agree. And in general, this sort of thing is exactly my core concern about adding typed throws to the language: I am completely certain that many programmers will add typed throws annotations because they're programmers and thus, well, probably a little obsessive/compulsive, and they're trying to precisely document the behavior of their function without necessarily thinking about the usefulness of that information for their clients and (if they're writing a library; and really you should almost always be writing code as if you're writing a library) whether they're actually willing to commit to that behavior in their interface. For those programmers, typed throws is just going to box them in and force them into anti-patterns in the long run.

In the vast majority of use-cases, clients are not going to exhaustively handle all errors — they will always have some generic fall-back. That is not pessimism, it's actually the natural result of the complicated world we live in, where code can fail for a huge host of reasons and most callers won't have meaningful special-case behavior for all of them. (On most operating systems, opening a file or a network connection can fail because you ran out of file descriptors. You're seriously telling me that you're going to add a special case to your error logic for that?) Go look at the actual error types that people use in most typed-throws situations and try to tell me I'm wrong — they probably have like twenty alternatives, and solidly a quarter or more of them will just be embedding some other arbitrarily-complex or stringly-typed error value.

The real use case for typed throws is when you have something like a parser library that really does only fail in a fixed number of semantically distinct ways, and you both (1) actually care about enforcing that in the implementation and making sure that other errors are handled internally and (2) you really do expect clients to exhaustively switch over the error at some point. That's important. But I continue to think that if adding better support for that use case misleads other programmers into thinking they should use typed throws, we will have made the language worse overall.

John.

2017-02-18 18:27 GMT+03:00 Karl Wagner <razielim@gmail.com <mailto:razielim@gmail.com>>:

So, I’m not sure about what was decided last time, but my issues with this are:

- The thrown error type will become part of the ABI of the function. If you change the type of Error that is thrown, callers may not catch it. At the same time, if we make enums resilient by default and only allow specifying a single entire type, you will basically need one Error enum per function and it will need to be @fixed if you actually want to remove the catch-all block. Otherwise:

// Let’s say this isn’t @fixed...
enum CanFailError {
    errorOne
    errorTwo
}

func canFail() throws(CanFailError) { /* … */ }

do { try canFail() }
catch CanFailError {
    switch error {
        case .errorOne: /* handle error one */
        case .errorTwo: /* handle error two */
        default: /* handle possible new errors in later versions of the library */
    }
}

do { try canFail() }
catch .errorOne { /* handle error one */ }
catch .errorTwo { /* handle error two */ }
catch { /* handle possible new errors in later versions of the library */ }

- I usually have _semantic_ namespaces for Errors, rather than single types per implementation pattern. If we are adding strong annotations about which errors can be thrown, I’d quite like to incorporate that pattern. For example:

extension File {
@fixed enum OpeningError {
  case .invalidPath
  case .accessDenied // e.g. asking for write permissions for read-only file
}
@fixed enum ReadError {
  case .invalidOffset // past EOF
  case .deviceError // probably worth aborting the entire operation the read is part of
}

// - throws:
// - .OpeningError if the file can’t be opened
// - .ReadError if the read operation fails
func read(from offset: Int, into buffer: UnsafeBufferPointer<UInt8>) throws(OpeningError, ReadError) { /* … */ }
}

- I wonder if we could try something more ambitious. Since the list of thrown errors is resilience-breaking for the function, it is only beneficial for versioned and @inlineable functions. They should not be able to add new errors (they can remove them though, since errors are intended to be switched over). I wonder if we couldn’t introduce a small pattern grammar for our structured comments (isolated from the rest of the language) - it would be optional, but if you do list your errors, the compiler would validate that you do it exhaustively. Some patterns I would like are:

// - throws: - MyError.{errorOne, errorThree, errorFive}: Something bad || considered exhaustive
@inlineable public func canFail() throws {}

// - throws: - OpeningError: Computer says nooooo... || considered exhaustive if OpeningError is versioned or @fixed
// - * || other errors, requires “catch-all” by external callers
@inlineable public func canFail2() throws {}

If we want to get really clever, we can have the compiler automatically generate those error-lists for internal functions, so you would automatically get exhaustive error-handling within your own module.

- Karl

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

I agree, and that’s where I was going with it: I think that typed-throws should basically be something on the level of a stronger comment rather than something so definitive as the function’s ABI. That’s how it will be much of the time in practice, anyway.

I don’t believe having a single error type is really ideal for anything. We’ve basically whittled down the feature until it gets in the way. If every function is throwing its own enum or hidden under complex hierarchies of protocols, it becomes difficult to write helper routines which respond to common errors in certain ways (e.g. trying an operation if if failed because the network was down).

// Using one-enum per function

enum FunctionOneError: Error {
    case networkWasDown(shouldTryAgain: Bool)
    case otherReason
}
func functionOne() throws(FunctionOneError)

enum FunctionTwoError: Error {
    case networkWasDown(shouldTryAgain: Bool)
    case aDifferentReason
}
func functionTwo() throws(FunctionTwoError)

// How to use this information at a high level?

func retryIfNetworkDown(let attempts: Int = 3, work: ()throws->Void) rethrows -> Bool { // <- Can’t specify which errors we take, or which we rethrow
    for n in 0..<attempts {
        do { try work() }
        catch FunctionOneError.networkWasDown(let tryAgain) {
            if tryAgain, n<attempts { continue }
            else { return false }
        }
        catch FunctionTwoError.networkWasDown(let tryAgain) { // Needs to handle per-function errors :(
            if tryAgain, n<attempts { continue }
            else { return false }
        }
        catch { throw error }
    }
}

So I’ve heard people say you should create a protocol then, but that’s not really a convenient solution either...

protocol NetworkError {
    func wasNetworkDownAndShouldTryAgain() -> (Bool, Bool)
}

extension FunctionOneError: NetworkError {
    func wasNetworkDownAndShouldTryAgain() -> (Bool, Bool) {
        guard case .networkWasDown(let tryAgain) = self else { return (false, false) }
        return (true, tryAgain)
    }
}

// This needs to be done twice, too...

extension FunctionTwoError: NetworkError {
    func wasNetworkDownAndShouldTryAgain() -> (Bool, Bool) {
        guard case .networkWasDown(let tryAgain) = self else { return (false, false) }
        return (true, tryAgain)
    }
}

So I think it all descends in to lots of syntax for very marginal amounts of value. Typed-throws is never likely to be the wondrous 100% cross-library reliability guarantee that people dream of. I view it more like good documentation.

It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.

If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.

I don’t like any solution that is based on comments. I think that compiler should always ignore comments.

I agree. And in general, this sort of thing is exactly my core concern about adding typed throws to the language: I am completely certain that many programmers will add typed throws annotations because they're programmers and thus, well, probably a little obsessive/compulsive, and they're trying to precisely document the behavior of their function without necessarily thinking about the usefulness of that information for their clients and (if they're writing a library; and really you should almost always be writing code as if you're writing a library) whether they're actually willing to commit to that behavior in their interface. For those programmers, typed throws is just going to box them in and force them into anti-patterns in the long run.

In the vast majority of use-cases, clients are not going to exhaustively handle all errors — they will always have some generic fall-back. That is not pessimism, it's actually the natural result of the complicated world we live in, where code can fail for a huge host of reasons and most callers won't have meaningful special-case behavior for all of them. (On most operating systems, opening a file or a network connection can fail because you ran out of file descriptors. You're seriously telling me that you're going to add a special case to your error logic for that?) Go look at the actual error types that people use in most typed-throws situations and try to tell me I'm wrong — they probably have like twenty alternatives, and solidly a quarter or more of them will just be embedding some other arbitrarily-complex or stringly-typed error value.

I agree that overly specific API specs are a concern. There's still some documentational benefit to a typed error enum, even if it ultimately ends up containing a catch-all case, since it gives you the ability to also enumerate some number of discrete, realistically handleable cases. Sure, your network request running in an XPC service has infinitely many failure cases due to resource constraints or IPC failure or network chaos, but there's usually some number of domain-specific error cases too. And if we compare the proposed "you get to specify one error enum type" model to, say, Java or C++98's "you get to specify any number of error classes" model, I think that helps steer people away from the most grievous API mistakes in Java land, since instead of listing a closed set of concrete error classes directly in your API signature, you'll list those error cases in your enum definition, and in a resilient world, the enum will be "open" by default to external users, preventing it from being a permanent liability unless the user explicitly opted into closedness.

Realistically there are only rarely actionable error cases and usually only one or two; everything else just bubbles up to the top of the current operation where you retry or report a permanent failure. If the enum is open then you need a default catch-all error handling case anyway, so what benefit is there to typed throws that you don’t get with documentation comments?

I’m positive John is right about what will happen in practice.

In the discussions around Rust's error handling conventions, they recognized this pattern of APIs either raising some number of layer-appropriate errors or carrying forward the failure modes of the layers below them, and they developed a convention for errors wrapping other errors. It would be reasonable to say we should do the same thing as part of the typed errors design. If we were to generalize enum subtyping beyond Optional, that might be one way to go about it, letting an enum wrap its underlying layers' failure cases as subtypes.

-Joe

Yeah but Rust has a macro system so error-chain makes re-wrapping trivial. A simple `chain_err(|| “my new error message”)` is enough to define and “throw” a new error that wraps the underlying error.

Russ

···

On Feb 20, 2017, at 1:15 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 20, 2017, at 9:57 AM, John McCall via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 19, 2017, at 3:04 PM, Anton Zhilin via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.

If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.

I don’t like any solution that is based on comments. I think that compiler should always ignore comments.

I agree. And in general, this sort of thing is exactly my core concern about adding typed throws to the language: I am completely certain that many programmers will add typed throws annotations because they're programmers and thus, well, probably a little obsessive/compulsive, and they're trying to precisely document the behavior of their function without necessarily thinking about the usefulness of that information for their clients and (if they're writing a library; and really you should almost always be writing code as if you're writing a library) whether they're actually willing to commit to that behavior in their interface. For those programmers, typed throws is just going to box them in and force them into anti-patterns in the long run.

In the vast majority of use-cases, clients are not going to exhaustively handle all errors — they will always have some generic fall-back. That is not pessimism, it's actually the natural result of the complicated world we live in, where code can fail for a huge host of reasons and most callers won't have meaningful special-case behavior for all of them. (On most operating systems, opening a file or a network connection can fail because you ran out of file descriptors. You're seriously telling me that you're going to add a special case to your error logic for that?) Go look at the actual error types that people use in most typed-throws situations and try to tell me I'm wrong — they probably have like twenty alternatives, and solidly a quarter or more of them will just be embedding some other arbitrarily-complex or stringly-typed error value.

I agree that overly specific API specs are a concern. There's still some documentational benefit to a typed error enum, even if it ultimately ends up containing a catch-all case, since it gives you the ability to also enumerate some number of discrete, realistically handleable cases. Sure, your network request running in an XPC service has infinitely many failure cases due to resource constraints or IPC failure or network chaos, but there's usually some number of domain-specific error cases too.

I definitely agree with the documentational benefits of declaring interesting error cases, and I agree that most libraries will add new domain-specific errors, too. I just don't see the actual benefit to either API authors or clients of saying "I will only throw errors that look like *this*" and then *this* turns out to be an enum with ten interesting cases, most of which don't apply to the specific operation, and a bunch of generic wrapping cases that communicate very little; especially not compared to saying "in this interesting case I'll throw a FooError.bar" in the documentation. Especially since it's not obvious that *cases* are the right granularity for describing a specific kind of domain error, since a case can't be retroactively split into more- and less-informative sub-cases without adding language features that only I seem to be interested in. :)

And if we compare the proposed "you get to specify one error enum type" model to, say, Java or C++98's "you get to specify any number of error classes" model, I think that helps steer people away from the most grievous API mistakes in Java land, since instead of listing a closed set of concrete error classes directly in your API signature, you'll list those error cases in your enum definition, and in a resilient world, the enum will be "open" by default to external users, preventing it from being a permanent liability unless the user explicitly opted into closedness.

To be exact, resilience prevents it from being a permanent liability as long as you've thought ahead enough to use your own error type.

In the discussions around Rust's error handling conventions, they recognized this pattern of APIs either raising some number of layer-appropriate errors or carrying forward the failure modes of the layers below them, and they developed a convention for errors wrapping other errors. It would be reasonable to say we should do the same thing as part of the typed errors design. If we were to generalize enum subtyping beyond Optional, that might be one way to go about it, letting an enum wrap its underlying layers' failure cases as subtypes.

I agree that, if we added typed throws, there would be an immediate demand for enum subtyping as a way to make remapping less painful. We'd also want some way of allowing users to specify an *explicit* remapping function (presumably a case constructor in most cases); this could probably be a library function, though.

John.

···

On Feb 20, 2017, at 4:15 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 9:57 AM, John McCall via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 19, 2017, at 3:04 PM, Anton Zhilin via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote: