[Pitch] Typed throws

Wouldn’t that mean that you couldn’t use your Swift library in Objective-C anymore, at least the error type as an NSError?

···

--
Adrian Zubarev
Sent with Airmail

Am 17. Februar 2017 um 21:16:57, Tino Heth (2th@gmx.de) schrieb:

I thought it was going to be any one subtype of Error, be it a struct, an enum, or a protocol existential, or Error itself.

Imho we should remove the restriction that you can only throw Error-conforming types if typed throws are added:
It's a compatibility feature, and if you manually declare what can be thrown, you should be allowed to break with Objective-C.

As Error has no requirements at all, it looks like harassment, as its whole magic is neither visible nor necessary.

I meant to say “… more than one throwing error type …”

···

--
Adrian Zubarev
Sent with Airmail

Am 17. Februar 2017 um 22:48:35, Adrian Zubarev (adrian.zubarev@devandartist.com) schrieb:

No actually not, it was made up by the assumption that the proposed syntax would have more than one throwing that which was clarified by others to be incorrect. ;)

Thanks again for clarification.

--
Adrian Zubarev
Sent with Airmail

Am 17. Februar 2017 um 22:45:35, Joe Groff (jgroff@apple.com) schrieb:

On Feb 17, 2017, at 11:03 AM, Adrian Zubarev via swift-evolution <swift-evolution@swift.org> wrote:

I suggest we need to find a way to shorten the list of the possible error types with a the help of typeallias

extension MyError1: Error { ... }
extension MyError2: Error { ... }
extension MyError3: Error { ... }

typealias MyErrors = MyError1 | MyError2 | MyError3

func foo() throws(MyErrors) -> MyResult
func bar<T : Error>(_: () throws(T) -> Void) rethrows(MyErrors, T) -> MyResult
Do you actually need that? 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. If nothing else, you could `Either` your way to multiple errors if you really needed to.

IMO, if we accept a single error type per function, there could be a simpler model for this. We could say that the `throws` type is a generic parameter of all function types, and it defaults to the uninhabited `Never` type for functions that don't throw.

() -> () == () throws Never -> ()
() throws -> () == () throws Error -> ()

In this model, you'd get many benefits:

- `rethrows` could become first-class, reducing down to just polymorphic `throws`:

func foo(_: () throws -> ()) rethrows // Swift 3
func foo<T: Error>(_: () throws T -> ()) throws T // Swift X
func foo<T: Error>(_: () throws T -> ()) throws Either<MyErrors, T>

- Protocols could abstract over error handling; for instance, we could support throwing sequences:

protocol IteratorProtocol {
associatedtype Element
associatedtype Error: Swift.Error = Never

mutating func next() throws Error -> Element?
}

Separate of the type system model, the type *checking* model also deserves thorough consideration. Propagating the effects of possibly multiple error types propagating within a `do` block is much trickier than doing so as a single "throws" or not bit, especially if you want to be able to use type context in `catch` patterns or to implicitly propagate a narrower `throws` type out of the enclosing function.

-Joe

Several proposals will follow this one: allowing multiple error types,
removing Error, replacing rethrows, etc.
Those topics are more controversial, but fortunately for them, they mostly
add on top of the core feature being discussed.
So IMO, if a detail can be split into its own proposal, we should just do
it and forget about it for a little while.

···

2017-02-17 23:16 GMT+03:00 Tino Heth <2th@gmx.de>:

I thought it was going to be any one subtype of Error, be it a struct, an
enum, or a protocol existential, or Error itself.

Imho we should remove the restriction that you can only throw
Error-conforming types if typed throws are added:
It's a compatibility feature, and if you manually declare what can be
thrown, you should be allowed to break with Objective-C.

As Error has no requirements at all, it looks like harassment, as its
whole magic is neither visible nor necessary.

Wouldn’t that mean that you couldn’t use your Swift library in Objective-C anymore, at least the error type as an NSError?

That's the meaning of "break with Objective-C" here ;-) — but note that I wrote about allowing to do so, not forcing:
Now, we can only declare that something is thrown, and as interoperability is quite important, we have to assume it is needed.
As soon as you declare exactly what will be thrown, it should be up to you to decide if you need NSError-bridging.

Just to rephrase this a little bit.

- The list of thrown errors would not be part of the function’s signature. Instead, there is an ABI invariant that it throws an Error, and that it remains "switch-compatible". That’s a loose definition, but it covers things like throwing non-public Errors and later making them public while retaining ABI compatibility.
- It’s up to the library developer to maintain that invariant. They can remove errors, refine errors (e.g. “MyError” -> “MyError.{errorOne, errorThree}”), hide or reveal previously-hidden errors.
- If the errors are listed exhaustively, the compiler will generate an implicit catch-all to trap in case the library violates this invariant. This means they can become more specific while retaining ABI compatibility.

- We would put this information in the structured comments, because it can get long and complex and is always optional
- The compiler would ensure that their documentation is accurate, as it does any other part of the language.

- Karl

···

On 18 Feb 2017, at 16:27, Karl Wagner <karl.swift@springsup.com> wrote:

- 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

+1

-Chris

···

On Feb 17, 2017, at 11:29 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 17, 2017, at 1:24 PM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Let's not bring bikeshedding the commonly proposed and rejected union spelling into this.
Typed throws would be a nice addition, assuming that the core team finds it in scope for phase 2. It seems only logical that any type can be thrown (i.e. conforms to Error) should be permitted to be listed in `throws()`.

Agree. Typed throws should have a single thrown type. Making it more convenient to throw and catch more than one error type with typed throws without having to manually create a wrapper is an orthogonal issue. David Owens convinced me of this last year when we had a thread on the topic.

I really like this. Seems much more elegant and simple this way

+1

···

Sent from my iPad

On Feb 19, 2017, at 11:34 AM, Brandon Knope via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 17, 2017, at 4:45 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 17, 2017, at 11:03 AM, Adrian Zubarev via swift-evolution <swift-evolution@swift.org> wrote:

I suggest we need to find a way to shorten the list of the possible error types with a the help of typeallias

extension MyError1: Error { ... }
extension MyError2: Error { ... }
extension MyError3: Error { ... }

typealias MyErrors = MyError1 | MyError2 | MyError3

func foo() throws(MyErrors) -> MyResult
func bar<T : Error>(_: () throws(T) -> Void) rethrows(MyErrors, T) -> MyResult

Do you actually need that? 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. If nothing else, you could `Either` your way to multiple errors if you really needed to.

IMO, if we accept a single error type per function, there could be a simpler model for this. We could say that the `throws` type is a generic parameter of all function types, and it defaults to the uninhabited `Never` type for functions that don't throw.

() -> () == () throws Never -> ()
() throws -> () == () throws Error -> ()

In this model, you'd get many benefits:

- `rethrows` could become first-class, reducing down to just polymorphic `throws`:

func foo(_: () throws -> ()) rethrows // Swift 3
func foo<T: Error>(_: () throws T -> ()) throws T // Swift X
func foo<T: Error>(_: () throws T -> ()) throws Either<MyErrors, T>

- Protocols could abstract over error handling; for instance, we could support throwing sequences:

protocol IteratorProtocol {
  associatedtype Element
  associatedtype Error: Swift.Error = Never

  mutating func next() throws Error -> Element?
}

Separate of the type system model, the type *checking* model also deserves thorough consideration. Propagating the effects of possibly multiple error types propagating within a `do` block is much trickier than doing so as a single "throws" or not bit, especially if you want to be able to use type context in `catch` patterns or to implicitly propagate a narrower `throws` type out of the enclosing function.

-Joe
_______________________________________________
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

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.

···

2017-02-18 18:27 GMT+03:00 Karl Wagner <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

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

···

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:

--
-Dave

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

It seems to me that both can be true. It is up to the author to
determine which applies in a given use case. I really don't think
Swift should require those who understand where it is beneficial to
use non-throwing functions that return a Result type if they wish to
realize the benefits when they are relevant.

My understanding of Chris's meaning of progressive disclosure is that
the language should have powerful and general tools at its foundation
that can be ignored until they are necessary for a particular problem.

For what problem is this feature necessary?

I was speaking about progressive disclosure generally. You are right
that typed errors are not *necessary* for any problems. Of course
that can be said about many language features. Perhaps I should have
said "until they add value or clarity”.

In this case the powerful and general tool is typed errors. The
syntactic sugar allowing you to omit a type and therefore throw any
error (typed as Error) allows users to ignore the more general tool
when it isn't providing value.

We should focus on educating users who wish to use this tool about the
tradeoffs involved and how to think about what error type might be
appropriate in different use cases rather than introduce an arbitrary
(i.e. not technical) limitation prohibiting typed errors just because
they can be badly used. Understanding the tradeoffs is certainly not
a beginner topic, but there are plenty of things in Swift that are not
beginner topics.

In my experience, given an opportunity to encode something in the type
system or categorize and annotate things using language constructs, most
users will. That's usually a great instinct, but not in this case, IMO.
The hard problems of error recovery involve maintaining your program
invariants, but this feature contributes nothing toward that end. The
one thing it *could* improve is the quality of error reporting to end
users, but in practice that ends up devolving to dynamic lookups due to
the need for localization. So we truly gain very little from encoding
this feature in the type system.

Error reporting and recovery are precisely where type errors can help.
I’ll give an example below.

This particular feature is viral in the same sense as C++ const, so I
predict it will either see widespread use, which IMO would be harmful,
or everyone will learn to avoid it. Either way, it seems like a bad
investment for the language.

I understand why you might make this comparison but I think there is
an important difference.

If I receive a const input I can only give that input to other things
that take a const. It restricts my dependencies (what I can do with
the input), or conversely I have to abandon const or cast it away (!)
because my dependency isn’t declared as taking const.

With typed errors the situation is a little bit different. My
signature is much more independent from that of my dependencies.

For example, I might have some dependencies which my users would
prefer to not be tightly coupled with that happen to throw errors.
For example, maybe I depend on an IO library and a parsing library.
These dependencies are subject to change in the future. Rather than
let the errors propagate directly I wrap them in `enum MyLIbraryError
{ case IOError(Error); case ParsingError(Error) }`.

This could provide a meaningful abstraction / grouping that helps the
client know what might or might not solve the problem.

And it might not. It might turn out that something you have to call
throws an error that doesn't fit into either case. Remember, the things
you call may be closures or methods supplied to you by your clients.
Then you have to resort to the equivalent of casting away const.

It might be worth exposing the error type to callers giving them an
easier way to cover different high-level causes of an error. In this
example, I can perform the grouping even if my dependencies throw
untyped errors.

Lots of things might be worth doing. I'm saying you need a much more
compelling case that this *is* worth doing before I'd personally risk
adding it to the language, when experience and, frankly, plain logic,
show this kind of feature to have been problematic.

Even if this enum is exposed resiliently it still provides a
significantly better experience for my users than an untyped error.
They know that these are the major categories of error that they
should be thinking about when using my library and have an easy way to
catch each kind of error.

You can give users nearly the same value by simply documenting that
your library throws MyLibraryError, without any of the risks.

They might also decide to automatically retry an operation or not
depending on what kind of error occurred.

This kind of wrapping and grouping can obviously be performed without
typed errors but IMO is better with typed errors where the types never
get out of sync with the code (as documentation can)

With resilient enums, of course they do get out of sync.

and we have better tool integration. Also, when working in the same
module (or with a non-resilient library error) we get the ability to
catch errors exhaustively without needing an “unknown error” clause.
This *does not* mean every individual error is handled directly, but
that they are meaningfully grouped in a way that allows us to cover
each possible grouping.

Any problems this feature has with being viral would be one of user
expectations and community culture. It wouldn’t be a strictly
technical virality that sets up a kind of dependency between users of
my function and implementation details of my function.

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

···

On Feb 27, 2017, at 4:20 PM, Dave Abrahams <dabrahams@apple.com> wrote:
on Mon Feb 27 2017, Matthew Johnson <matthew-AT-anandabits.com <http://matthew-at-anandabits.com/&gt;&gt; wrote:

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

On Feb 27, 2017, at 12:32 AM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

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

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

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.