Proposal: Typed throws


(Adrian Kashivskyy) #1

I'm sure you heard this request like a billion times already, but I'd like to be official and make a proper proposal for that. So having said that, I hereby propose adding support for typed `throws` annotations.

If a function can throw, it is often known what type of error it may throw. Consider this piece of code:

enum NetworkError: ErrorType {
  case RequestTimeout
  case UnreachableHost
}

enum SearchError: ErrorType {
  case InvalidQuery
}

func search(query: String) throws {}

func m() {
  do {
    try search("foo")
  } catch SearchError.InvalidQuery {
    print("your query is invalid")
  } catch is NetworkError {
    print("please check your internet connection")
  } catch {
    print("an unknown error occurred") // ???
  }
}

In order for `do-catch` to be exhaustive, the vague `catch` block is required by the compiler, and inside of it, one has literally no idea what kind of error they deal with. This is useless or even dangerous for error handling, because the last `catch` often looks like this:

catch {
  // don't know what that is, let's ignore that
}

The situation above can be resolved by introducing typed error handling, so that the `search` function may be refactored to:

func search(query: String) throws SearchError, NetworkError {}

Then, the requirement for last, opaque `catch` block can be completely removed as the compiler will make sure that only errors of the allowed types be thrown from `search`.

I will be happy to hear your comments on this.

–––––

As a bonus, I prepared a short FAQ for my proposal:

Q: What if my function throws multiple error types?

Use comma-separated list of error types.

func f() throws SomeError, OtherError {}

Q: What if I `try` a function which throws something completely different?

Then you have three possibilities:

1. You resolve the error in scope of the function, inside a `catch` block.
2. You convert it into a compatible type and `throw` it.
3. You annotate your `func` to be throwing that particular error as well.

Q: What about `rethrows`?

`rethrows` should become generic and throw anything that the closure arguments can throw.

func f(g: () throws SomeError -> Void, h: () throws OtherError -> Void) rethrows {}

// behaves like

func f(g: () throws SomeError -> Void, h: () throws OtherError -> Void) throws SomeError, OtherError {}

Q: What if I want to use the old behavior?

Just annotate as `throws ErrorType` and you can throw anything from there.

func f() throws ErrorType {}

Q: Is the failure path really as important as the success path?

Yes, it is.

Pozdrawiam – Regards,
Adrian Kashivskyy


Why doesn't Swift have explicit throwables like Java
(Adrian Kashivskyy) #2

Anyone has any thoughts on this? I didn't expect this topic to die without any response...

Pozdrawiam – Regards,
Adrian Kashivskyy

···

Wiadomość napisana przez Adrian Kashivskyy <adrian.kashivskyy@me.com> w dniu 04.12.2015, o godz. 01:43:

I'm sure you heard this request like a billion times already, but I'd like to be official and make a proper proposal for that. So having said that, I hereby propose adding support for typed `throws` annotations.

If a function can throw, it is often known what type of error it may throw. Consider this piece of code:

enum NetworkError: ErrorType {
  case RequestTimeout
  case UnreachableHost
}

enum SearchError: ErrorType {
  case InvalidQuery
}

func search(query: String) throws {}

func m() {
  do {
    try search("foo")
  } catch SearchError.InvalidQuery {
    print("your query is invalid")
  } catch is NetworkError {
    print("please check your internet connection")
  } catch {
    print("an unknown error occurred") // ???
  }
}

In order for `do-catch` to be exhaustive, the vague `catch` block is required by the compiler, and inside of it, one has literally no idea what kind of error they deal with. This is useless or even dangerous for error handling, because the last `catch` often looks like this:

catch {
  // don't know what that is, let's ignore that
}

The situation above can be resolved by introducing typed error handling, so that the `search` function may be refactored to:

func search(query: String) throws SearchError, NetworkError {}

Then, the requirement for last, opaque `catch` block can be completely removed as the compiler will make sure that only errors of the allowed types be thrown from `search`.

I will be happy to hear your comments on this.

–––––

As a bonus, I prepared a short FAQ for my proposal:

Q: What if my function throws multiple error types?

Use comma-separated list of error types.

func f() throws SomeError, OtherError {}

Q: What if I `try` a function which throws something completely different?

Then you have three possibilities:

1. You resolve the error in scope of the function, inside a `catch` block.
2. You convert it into a compatible type and `throw` it.
3. You annotate your `func` to be throwing that particular error as well.

Q: What about `rethrows`?

`rethrows` should become generic and throw anything that the closure arguments can throw.

func f(g: () throws SomeError -> Void, h: () throws OtherError -> Void) rethrows {}

// behaves like

func f(g: () throws SomeError -> Void, h: () throws OtherError -> Void) throws SomeError, OtherError {}

Q: What if I want to use the old behavior?

Just annotate as `throws ErrorType` and you can throw anything from there.

func f() throws ErrorType {}

Q: Is the failure path really as important as the success path?

Yes, it is.

Pozdrawiam – Regards,
Adrian Kashivskyy

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


(John McCall) #3

Patience. :slight_smile: We’ve been getting a lot of proposals, and it hasn’t even been a full day yet.

I do think that there’s a place for typed throws. There are some very narrow use cases, usually where error handling is being employed as a sort of alternate control scheme rather than a mechanism for reporting “errors” per se, where it’s nice to both:

  - inform callers that the function only “fails” in the prescribed ways

  - statically enforce that general errors aren’t accidentally getting mixed in

You might imagine using it in a recursive-descent parser, for example, although even there I’m not convinced.

However, I’m still reluctant to fully embrace the feature, because I think it’s far too tempting to try to use it for actual error handling, and that’s a job that it’s really, really poorly-suited for.

Let’s get one thing out of the way first. There are some operations that make sense to be able to do on a generic error — crucially, formatting it for display in various ways — and right now the ErrorType protocol doesn’t expose API for that. That’s something we perhaps need to work on.

Beyond that, however, it has always been unclear to me what exactly programmers expect to do with this additional typing information on errors, besides pedantically copying it all over the place and feeling pleased. Libraries can fail in a lot of different ways. It’s inevitable that that set will grow in ways that neither the library’s authors nor its users are necessarily anticipating. Even putting evolution aside, the list of failures is almost certainly impractical to exhaustively pattern-match over, and doing so couples you very tightly to the implementation of the library and everything it uses. Furthermore, in practice nearly every “exhaustively enumerable” error type I’ve ever seen has at least one (and often two or three) generic, catch-all cases used to encode arbitrary extra kinds of error, either dynamically-typed (e.g. carrying an ErrorType) or completely untyped (e.g. carrying a String); and not only does this make it impossible to exhaustively enumerate the actual kinds of failure, but it actually makes it more difficult to detect specific failures, by making it more likely that the error you’re actually trying to detect and special-case will be stored at the end of a rather complicated path of wrapping errors.

And this isn’t even just about third-party libraries, because if there’s any lesson from the last fifty years of experience that's broadly applicable across programming languages, it’s that you really do need to think about how your program breaks down into components, and understand how those components work with each other essentially as if they were separable libraries, or else you will always, always run into a wall as you find that the complexity of your comprehension has failed to scale with the complexity of your code.

So I continue to feel that the pattern of “recognize a few special cases and then be prepared to deal with other failures generically” is basically the right approach for ordinary error-handling in Swift, and I think it’s pretty well-served by the current design.

John.

···

On Dec 4, 2015, at 10:18 AM, Adrian Kashivskyy <adrian.kashivskyy@me.com> wrote:
Anyone has any thoughts on this? I didn't expect this topic to die without any response…


(David Owens II) #4

For the most part, I feel that typed errors are more of a response to how we need to handle errors within Swift today.

    var vendingMachine = VendingMachine()
    vendingMachine.coinsDeposited = 8
    
    do {
        try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)
    } catch VendingMachineError.InvalidSelection {
        print("Invalid Selection.")
    } catch VendingMachineError.OutOfStock {
        print("Out of Stock.")
    } catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
        print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
    } catch { fatalError("this is always needed…”) }

So I continue to feel that the pattern of “recognize a few special cases and then be prepared to deal with other failures generically” is basically the right approach for ordinary error-handling in Swift, and I think it’s pretty well-served by the current design.

Possibly… but I think the counter argument is that introducing a new error type that can happen **should** be a breaking change. So the `catch` with no pattern becomes a pattern that is “yeah, some error may happen at a later date, but since we have no idea what it is, it may be benign or it may be catastrophic. So, should it be handled with a `fatalError()` or should it be equivalent to a no-op?

To me, the question really becomes: does a non-typed error-like system really make sense in Swift? Or in other words, if interop with ObjC was not required, would `throw` exist as it does today?

It would seem that something has to give: either something like a `Result`/`Error` type should be used instead with some nicer syntax to handle it, or `throws` should really become typed. Leaving it as-is, seems to be the worst of both worlds.

-David

···

On Dec 4, 2015, at 11:08 AM, John McCall <rjmccall@apple.com> wrote:

On Dec 4, 2015, at 10:18 AM, Adrian Kashivskyy <adrian.kashivskyy@me.com> wrote:
Anyone has any thoughts on this? I didn't expect this topic to die without any response…

Patience. :slight_smile: We’ve been getting a lot of proposals, and it hasn’t even been a full day yet.

I do think that there’s a place for typed throws. There are some very narrow use cases, usually where error handling is being employed as a sort of alternate control scheme rather than a mechanism for reporting “errors” per se, where it’s nice to both:

- inform callers that the function only “fails” in the prescribed ways

- statically enforce that general errors aren’t accidentally getting mixed in

You might imagine using it in a recursive-descent parser, for example, although even there I’m not convinced.

However, I’m still reluctant to fully embrace the feature, because I think it’s far too tempting to try to use it for actual error handling, and that’s a job that it’s really, really poorly-suited for.

Let’s get one thing out of the way first. There are some operations that make sense to be able to do on a generic error — crucially, formatting it for display in various ways — and right now the ErrorType protocol doesn’t expose API for that. That’s something we perhaps need to work on.

Beyond that, however, it has always been unclear to me what exactly programmers expect to do with this additional typing information on errors, besides pedantically copying it all over the place and feeling pleased. Libraries can fail in a lot of different ways. It’s inevitable that that set will grow in ways that neither the library’s authors nor its users are necessarily anticipating. Even putting evolution aside, the list of failures is almost certainly impractical to exhaustively pattern-match over, and doing so couples you very tightly to the implementation of the library and everything it uses. Furthermore, in practice nearly every “exhaustively enumerable” error type I’ve ever seen has at least one (and often two or three) generic, catch-all cases used to encode arbitrary extra kinds of error, either dynamically-typed (e.g. carrying an ErrorType) or completely untyped (e.g. carrying a String); and not only does this make it impossible to exhaustively enumerate the actual kinds of failure, but it actually makes it more difficult to detect specific failures, by making it more likely that the error you’re actually trying to detect and special-case will be stored at the end of a rather complicated path of wrapping errors.

And this isn’t even just about third-party libraries, because if there’s any lesson from the last fifty years of experience that's broadly applicable across programming languages, it’s that you really do need to think about how your program breaks down into components, and understand how those components work with each other essentially as if they were separable libraries, or else you will always, always run into a wall as you find that the complexity of your comprehension has failed to scale with the complexity of your code.

So I continue to feel that the pattern of “recognize a few special cases and then be prepared to deal with other failures generically” is basically the right approach for ordinary error-handling in Swift, and I think it’s pretty well-served by the current design.

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


(John McCall) #5

For the most part, I feel that typed errors are more of a response to how we need to handle errors within Swift today.

   var vendingMachine = VendingMachine()
   vendingMachine.coinsDeposited = 8

   do {
       try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)
   } catch VendingMachineError.InvalidSelection {
       print("Invalid Selection.")
   } catch VendingMachineError.OutOfStock {
       print("Out of Stock.")
   } catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
       print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
   } catch { fatalError("this is always needed…”) }

But this is printing. Of course you should be able to generically display an error, but you don’t need static typing for that. Also, I certainly hope you are not actually repeating all this stuff at every catch site.

So I continue to feel that the pattern of “recognize a few special cases and then be prepared to deal with other failures generically” is basically the right approach for ordinary error-handling in Swift, and I think it’s pretty well-served by the current design.

Possibly… but I think the counter argument is that introducing a new error type that can happen **should** be a breaking change.

This is abstract reasoning without any real consideration for what it’s going to do to actual code. My argument here is that it’s very easy to fall into a trap where you think this information is useful, but in practice it just leads to a lot of redundancy and boilerplate which, overall, actually means you’re handling errors *worse* than you were before.

So the `catch` with no pattern becomes a pattern that is “yeah, some error may happen at a later date, but since we have no idea what it is, it may be benign or it may be catastrophic. So, should it be handled with a `fatalError()` or should it be equivalent to a no-op?

To me, the question really becomes: does a non-typed error-like system really make sense in Swift? Or in other words, if interop with ObjC was not required, would `throw` exist as it does today?

Yes. It is absolutely the case that libraries are not going to commit to a generating a small, fixed set of failures. Please feel free to try to find an exhaustive error enum that isn’t littered with vague catch-alls. Library authors often don’t want to commit to their current set of failures precisely *because* they feel that their current errors are too loosely typed, and they’re holding out hope that some day they’ll come along and make them more specific.

It’s nice to say things like library authors should be encouraged to think more carefully about what errors they generate, and I don’t disagree, but it is not reasonable language design to demand and expect that programmers suddenly start putting vastly more effort into this sort of thing. Honestly, I am curious what environment people are thinking of when they say that it’s been successful elsewhere, because whenever I see something like Result<T, SomeErrorType>, I take a look at SomeErrorType and almost always find that it is extremely loosely specified.

John.

···

On Dec 4, 2015, at 11:36 AM, David Owens II <david@owensd.io> wrote:

It would seem that something has to give: either something like a `Result`/`Error` type should be used instead with some nicer syntax to handle it, or `throws` should really become typed. Leaving it as-is, seems to be the worst of both worlds.

-David

On Dec 4, 2015, at 11:08 AM, John McCall <rjmccall@apple.com> wrote:

On Dec 4, 2015, at 10:18 AM, Adrian Kashivskyy <adrian.kashivskyy@me.com> wrote:
Anyone has any thoughts on this? I didn't expect this topic to die without any response…

Patience. :slight_smile: We’ve been getting a lot of proposals, and it hasn’t even been a full day yet.

I do think that there’s a place for typed throws. There are some very narrow use cases, usually where error handling is being employed as a sort of alternate control scheme rather than a mechanism for reporting “errors” per se, where it’s nice to both:

- inform callers that the function only “fails” in the prescribed ways

- statically enforce that general errors aren’t accidentally getting mixed in

You might imagine using it in a recursive-descent parser, for example, although even there I’m not convinced.

However, I’m still reluctant to fully embrace the feature, because I think it’s far too tempting to try to use it for actual error handling, and that’s a job that it’s really, really poorly-suited for.

Let’s get one thing out of the way first. There are some operations that make sense to be able to do on a generic error — crucially, formatting it for display in various ways — and right now the ErrorType protocol doesn’t expose API for that. That’s something we perhaps need to work on.

Beyond that, however, it has always been unclear to me what exactly programmers expect to do with this additional typing information on errors, besides pedantically copying it all over the place and feeling pleased. Libraries can fail in a lot of different ways. It’s inevitable that that set will grow in ways that neither the library’s authors nor its users are necessarily anticipating. Even putting evolution aside, the list of failures is almost certainly impractical to exhaustively pattern-match over, and doing so couples you very tightly to the implementation of the library and everything it uses. Furthermore, in practice nearly every “exhaustively enumerable” error type I’ve ever seen has at least one (and often two or three) generic, catch-all cases used to encode arbitrary extra kinds of error, either dynamically-typed (e.g. carrying an ErrorType) or completely untyped (e.g. carrying a String); and not only does this make it impossible to exhaustively enumerate the actual kinds of failure, but it actually makes it more difficult to detect specific failures, by making it more likely that the error you’re actually trying to detect and special-case will be stored at the end of a rather complicated path of wrapping errors.

And this isn’t even just about third-party libraries, because if there’s any lesson from the last fifty years of experience that's broadly applicable across programming languages, it’s that you really do need to think about how your program breaks down into components, and understand how those components work with each other essentially as if they were separable libraries, or else you will always, always run into a wall as you find that the complexity of your comprehension has failed to scale with the complexity of your code.

So I continue to feel that the pattern of “recognize a few special cases and then be prepared to deal with other failures generically” is basically the right approach for ordinary error-handling in Swift, and I think it’s pretty well-served by the current design.

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


(David Owens II) #6

This was a sample from the Swift docs: https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html#//apple_ref/doc/uid/TP40014097-CH42-ID508. I added the missing final `catch` that is required with Swift today.

The point being, the error enum is fully defined but we still need the final `catch` at the end because there is no type information on the `throws` annotation for `buyFavoriteSnack`. So unlike the case when dealing with enums and switch-statements, we lose all compile-time information about coverage of error states when we could know them. The current implementation of `throws` is the only (as far as I can tell) place in Swift that turns a compile-time validation problem into a run-time validation problem.

That’s my concern.

Instead, if we could annotate throws, we could move this into a compile-time validation.

    enum VendingMachineError: ErrorType {
        case InvalidSelection
        case InsufficientFunds(coinsNeeded: Int)
        case OutOfStock
    }

    func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws VendingMachineError {
        let snackName = favoriteSnacks[person] ?? "Candy Bar"
        try vendingMachine.vend(itemNamed: snackName)
    }

This allows the compiler to validate the only error information leaving this call site is a `VendingMachineError` and it allows all callers to ensure that they are indeed handling the all of the errors for `VendingMachineError`.

To me, that is a very practical and pragmatic problem and is fundamentally no different than the treatment of exhaustive switch cases needed when dealing with other enum values.

Instead, if I turned the `buyFavoriteSnack` into this:

    enum Error<ErrorType> {
        case Ok,
        case Error(ErrorType)
    }

    func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) -> Error<VendingMachineError>

I then get to leverage the compiler to check that I’m indeed handling all of the VendingMachineErrors.

Yes, I do realize that multiple error types would complicate that, but honestly, I’d rather than the limitation that only a single error type can be propagated up the callstack than to have the ability to have no typed error information at all.

Of course, I can do this myself, but the language is going to win so I still have to deal with how `throws` is treated within the language as a whole.

-David

···

On Dec 4, 2015, at 11:54 AM, John McCall <rjmccall@apple.com> wrote:

On Dec 4, 2015, at 11:36 AM, David Owens II <david@owensd.io> wrote:
For the most part, I feel that typed errors are more of a response to how we need to handle errors within Swift today.

  var vendingMachine = VendingMachine()
  vendingMachine.coinsDeposited = 8

  do {
      try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)
  } catch VendingMachineError.InvalidSelection {
      print("Invalid Selection.")
  } catch VendingMachineError.OutOfStock {
      print("Out of Stock.")
  } catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
      print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
  } catch { fatalError("this is always needed…”) }

But this is printing. Of course you should be able to generically display an error, but you don’t need static typing for that. Also, I certainly hope you are not actually repeating all this stuff at every catch site.


(Matthew Johnson) #7

Beyond that, however, it has always been unclear to me what exactly programmers expect to do with this additional typing information on errors, besides pedantically copying it all over the place and feeling pleased. Libraries can fail in a lot of different ways. It’s inevitable that that set will grow in ways that neither the library’s authors nor its users are necessarily anticipating.

So I continue to feel that the pattern of “recognize a few special cases and then be prepared to deal with other failures generically” is basically the right approach for ordinary error-handling in Swift, and I think it’s pretty well-served by the current design.

I am a big proponent of typed throws. IMO error results are every bit as important a part of an API contract as success results. Sometimes erasing type information is the right API contract and sometimes it is not. I don’t think requiring type information to be erased is the right way to go. It should be a choice the language allows library authors to make for themselves.

I have worked in many languages over the last 20 years and one complaint I have nearly always had is that documentation of error cases is quite poor. This problem has been significant enough that I think it warrants a language solution. Typed errors provide compiler-enforced documentation about what errors can arise from a call. The value of this compiler-enforced documentation should not be underestimated.

If the language evolves to support typed errors I believe we would learn how and when it is best to provide specific error types and when to employ type erasure / generic errors. The range of options would encourage thoughtful library authors to consider their approach carefully and likely lead to solutions that facilitate better error handling in code that uses the library.

One idea that I think merits consideration is the approach Rust uses to translate lower level errors representing implementation details into higher level errors represented in the API contract at module boundaries. A well-designed language feature along these lines would go a long way towards addressing the complexity of “previously unanticipated errors”, would provide compiler enforced documentation, and would allow library authors to present a well-defined contract for error conditions as well as success conditions.

I hope this at least begins to answer the question of what programmers might hope to achieve with additional typing information on errors.

Matthew


(David Owens II) #8

I created a pull request for this issue: https://github.com/apple/swift-evolution/pull/26

There was a question on GitHub:

The proposal seems to be ignoring the negative implementation experience in Java. Why is Swift different?

*Moving the discussion back to the alias*

···

--

I guess first and foremost, the proposal basically boils down to a syntactical sugar request for what could be considered `Error<T>` and `Result<T, U>` types as the realized return values.

The second fundamental ask is this: give us the ability to handle errors in a way that we can verify all of the potential codepaths with the compiler.

My experience in Java was many years ago, so I can only speak to what I remember, but what I do remember was the proliferation of exceptions for every little minute thing. In the end, they became a mechanism for control flow instead of for actual error cases.

An example would be a `FileNotFoundException`. Is it really exceptional that a file doesn't exist? No, obviously not. Bad API decisions cannot help you abuse language constructs.

The other problem seemed to be the shear number of exceptions that would be marked as throwable from any particular method. In the proposal, I specifically call out only allowing a single type. The purpose is to avoid such a proliferation problem.

This is what all of my do-catch code boils down to:

do {
    try f()
}
catch KnownError.Case1 { }
catch KnownError.ThatsAllOfThem {}
catch { fatalError("Well... if someone else adds another case, I better crash here and hopefully I'll see it before my customers do") }

Now, if I chose to use a catch-all `catch` clause, then that's fine. However, Swift forces me to do it today. So my option is to either:

1. Crash the user's app so that a potentially subtle bug doesn't propagate through the app causing a more serious error, or
2. Roll the dice and see what happens. Hopefully I have telemetry data here so I know when these handlers are happening, because the compiler cannot tell me.

If you have another option, I'm willing to hear it.

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

On Dec 4, 2015, at 11:54 AM, John McCall <rjmccall@apple.com> wrote:

On Dec 4, 2015, at 11:36 AM, David Owens II <david@owensd.io> wrote:
For the most part, I feel that typed errors are more of a response to how we need to handle errors within Swift today.

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8

do {
     try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.InvalidSelection {
     print("Invalid Selection.")
} catch VendingMachineError.OutOfStock {
     print("Out of Stock.")
} catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
     print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch { fatalError("this is always needed…”) }

But this is printing. Of course you should be able to generically display an error, but you don’t need static typing for that. Also, I certainly hope you are not actually repeating all this stuff at every catch site.

This was a sample from the Swift docs: https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html#//apple_ref/doc/uid/TP40014097-CH42-ID508. I added the missing final `catch` that is required with Swift today.

The point being, the error enum is fully defined but we still need the final `catch` at the end because there is no type information on the `throws` annotation for `buyFavoriteSnack`. So unlike the case when dealing with enums and switch-statements, we lose all compile-time information about coverage of error states when we could know them. The current implementation of `throws` is the only (as far as I can tell) place in Swift that turns a compile-time validation problem into a run-time validation problem.

That’s my concern.

Instead, if we could annotate throws, we could move this into a compile-time validation.

   enum VendingMachineError: ErrorType {
       case InvalidSelection
       case InsufficientFunds(coinsNeeded: Int)
       case OutOfStock
   }

   func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws VendingMachineError {
       let snackName = favoriteSnacks[person] ?? "Candy Bar"
       try vendingMachine.vend(itemNamed: snackName)
   }

This allows the compiler to validate the only error information leaving this call site is a `VendingMachineError` and it allows all callers to ensure that they are indeed handling the all of the errors for `VendingMachineError`.

To me, that is a very practical and pragmatic problem and is fundamentally no different than the treatment of exhaustive switch cases needed when dealing with other enum values.

Instead, if I turned the `buyFavoriteSnack` into this:

   enum Error<ErrorType> {
       case Ok,
       case Error(ErrorType)
   }

   func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) -> Error<VendingMachineError>

I then get to leverage the compiler to check that I’m indeed handling all of the VendingMachineErrors.

Yes, I do realize that multiple error types would complicate that, but honestly, I’d rather than the limitation that only a single error type can be propagated up the callstack than to have the ability to have no typed error information at all.

Of course, I can do this myself, but the language is going to win so I still have to deal with how `throws` is treated within the language as a whole.

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


(John McCall) #9

For the most part, I feel that typed errors are more of a response to how we need to handle errors within Swift today.

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8

do {
     try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.InvalidSelection {
     print("Invalid Selection.")
} catch VendingMachineError.OutOfStock {
     print("Out of Stock.")
} catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
     print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch { fatalError("this is always needed…”) }

But this is printing. Of course you should be able to generically display an error, but you don’t need static typing for that. Also, I certainly hope you are not actually repeating all this stuff at every catch site.

This was a sample from the Swift docs: https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html#//apple_ref/doc/uid/TP40014097-CH42-ID508. I added the missing final `catch` that is required with Swift today.

Ah, right, I knew I recognized that example.

This code is intended to demonstrate the ability to chain catches more than it is intended to demonstrate best practice for working with errors. In real code, this would be better done by giving VendingMachineError a conformance to CustomStringConvertible and just having this at the catch site:

  catch {
    print(error)
  }

The point being, the error enum is fully defined but we still need the final `catch` at the end because there is no type information on the `throws` annotation for `buyFavoriteSnack`. So unlike the case when dealing with enums and switch-statements, we lose all compile-time information about coverage of error states when we could know them. The current implementation of `throws` is the only (as far as I can tell) place in Swift that turns a compile-time validation problem into a run-time validation problem.

AnyObject and ImplicitlyUnwrappedOptional come immediately to mind. I expect that we will gradually more dynamic features, hopefully more like the former than the latter.

That’s my concern.

Instead, if we could annotate throws, we could move this into a compile-time validation.

I understand what you want to do. I am saying that, as a language designer, I am reluctant to add features that seem to be, frankly, aspirational bluffs.

I would like you to think about what your concrete error types will look like in their entirety and reconsider whether that static type would actually tell you anything useful. I certainly don’t believe that they’re worth the costs, which are quite high.

Yes, I do realize that multiple error types would complicate that, but honestly, I’d rather than the limitation that only a single error type can be propagated up the callstack than to have the ability to have no typed error information at all.

This is what I mean. Your code will use other code, which will produce its own errors. Your "single error type” will therefore end up providing at least one case, and probably several, that simply wraps other, opaque error types without providing any extra information. Thus, it will simply rebroadcast all of your library dependencies, and it will need to be modified whenever your implementation changes. You will have meaningless translation/wrapping code scattered at every library boundary just to appease the compiler, and anybody trying to recognize specific problems will have to pattern match through an arbitrarily-nested tree of such wrappers. This is all essentially an inevitable consequence of statically typed errors, and it is a bad outcome.

John.

···

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

On Dec 4, 2015, at 11:54 AM, John McCall <rjmccall@apple.com> wrote:

On Dec 4, 2015, at 11:36 AM, David Owens II <david@owensd.io> wrote:


(Chris Lattner) #10

FWIW, if you’re interested in the Swift error handling model, then this document is required reading:

John drove the design efforts for error handling in Swift 2 and wrote it to capture the thinking of the team. There is also this document that describes the intent of the feature as well, but it has been subsumed by The Swift Programming Langauge:

My take on it is that typed throws would be a great feature to add for all of the reasons you specify. However, rolling it out in Swift 2 would have been very problematic, because we didn’t (and still don’t) have the Swift 3 resilience model in place.

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”.

In the case of typed throws, you should only get the “exhaustiveness checking” of catch blocks if you’re catching an enum defined in your own module, or if it is a public enum from some other module that is defined as being fragile.

-Chris

···

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

do {
     try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.InvalidSelection {
     print("Invalid Selection.")
} catch VendingMachineError.OutOfStock {
     print("Out of Stock.")
} catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
     print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch { fatalError("this is always needed…”) }

But this is printing. Of course you should be able to generically display an error, but you don’t need static typing for that. Also, I certainly hope you are not actually repeating all this stuff at every catch site.

This was a sample from the Swift docs: https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html#//apple_ref/doc/uid/TP40014097-CH42-ID508. I added the missing final `catch` that is required with Swift today.

The point being, the error enum is fully defined but we still need the final `catch` at the end because there is no type information on the `throws` annotation for `buyFavoriteSnack`. So unlike the case when dealing with enums and switch-statements, we lose all compile-time information about coverage of error states when we could know them. The current implementation of `throws` is the only (as far as I can tell) place in Swift that turns a compile-time validation problem into a run-time validation problem.

That’s my concern.

Instead, if we could annotate throws, we could move this into a compile-time validation.