typed throws

Splitting this off into its own thread:

One related topic that isn’t discussed is type errors. Many third party libraries use a Result type with typed errors. Moving to an async / await model without also introducing typed errors into Swift would require giving up something that is highly valued by many Swift developers. Maybe Swift 5 is the right time to tackle typed errors as well. I would be happy to help with design and drafting a proposal but would need collaborators on the implementation side.

Typed throws is something we need to settle one way or the other, and I agree it would be nice to do that in the Swift 5 cycle.

For the purposes of this sub-discussion, I think there are three kinds of code to think about:
1) large scale API like Cocoa which evolve (adding significant functionality) over the course of many years and can’t break clients.
2) the public API of shared swiftpm packages, whose lifecycle may rise and fall - being obsoleted and replaced by better packages if they encounter a design problem.
3) internal APIs and applications, which are easy to change because the implementations and clients of the APIs are owned by the same people.

These each have different sorts of concerns, and we hope that something can start out as #3 but work its way up the stack gracefully.

Here is where I think things stand on it:
- There is consensus that untyped throws is the right thing for a large scale API like Cocoa. NSError is effectively proven here. Even if typed throws is introduced, Apple is unlikely to adopt it in their APIs for this reason.
- There is consensus that untyped throws is the right default for people to reach for for public package (#2).
- There is consensus that Java and other systems that encourage lists of throws error types lead to problematic APIs for a variety of reasons.
- There is disagreement about whether internal APIs (#3) should use it. It seems perfect to be able to write exhaustive catches in this situation, since everything in knowable. OTOH, this could encourage abuse of error handling in cases where you really should return an enum instead of using throws.
- Some people are concerned that introducing typed throws would cause people to reach for it instead of using untyped throws for public package APIs.

Even for non-public code. The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch. People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws. Every. Single. Time.

The issue I see here with non-typed errors is that relying on documentation is very error-prone. I'll give an example where I've used exhaustive error catching (but then again, I was generally the only one using exhaustive enum switches when we discussed those). I've made a simple library for reporting purchases to a server. The report needs to be signed using a certificate and there are some validations to be made.

This generally divides the errors into three logical areas - initialization (e.g. errors when loading the certificate, etc.), validation (when the document doesn't pass validation) and sending (network error, error response from the server, etc.).

Instead of using a large error enum, I've split this into three enums. At this point, especially for a newcommer to the code, he may not realize which method can throw which of these error enums.

I've found that the app can take advantage of knowing what's wrong. For example, if some required information is missing e.g. Validation.subjectNameMissing is thrown. In such case the application can inform the user that name is missing and it can offer to open UI to enter this information (in the case of my app, the UI for sending is in the document view, while the mentioned "subject name" information is in Preferences).

This way I exhaustively switch over the error enums, suggesting to the user solution of the particular problem without dumbing down to a message "Oops, something went wrong, but I have no idea what because this kind of error is not handled.".

Surely you must have a message like that. You're transmitting over a network, so all sorts of things can go wrong that you're not going to explain in detail to the user or have specific recoveries for. I would guess that have a generic handler for errors, and it has carefully-considered responses for specific failures (validation errors, maybe initialization errors) but a default response for others. Maybe you've put effort into handling more errors intelligently, trying to let fewer and fewer things end up with the default response — that's great, but it must still be there.

Well, Cocoa offers NSErrors with localized strings, so for network errors, I pass on the underlying NSError which can be used directly with NSAlert. (i.e. SendingError.networkError(underlyingError: NSError?)). But other than that I usually dumb it down to just .networkError - as the end user usually doesn't need to know any further details.

I can generally say that the only time I return "unknown error" from the library is when some of the Security framework functions such as SecTransformSetAttribute return Boolean and pass CFError via UnsafeMutablePointer - when the call returns false, I check whether the error pointer is not null - just to be a good citizen, not to crash in case the Security framework did not populate the error pointer. It generally should never happen, but it's better to handle the case than to crash.

That's one of the keys to my argument here: practically speaking, from the perspective of any specific bit of code, there will always be a default response, because errors naturally quickly tend towards complexity, far more complexity than any client can exhaustively handle. Typed throws just means that error types will all have catch-all cases like MyError.other(Error), which mostly seems counter-productive to me.

You usually know where this happened - the end user doesn't usually need to know the absolute details. Places, where you would use MyError.other(Error), it's just laziness to extend the enum a bit IMHO. For example, when you need to read some configuration file, you wrap it in try-catch statement and within the catch you log the underlying error, but throw .configurationFileReadError for simplicity. If it's important to include more information, you can break it to a few subcases.

I totally agree that we could do a lot more for documentation. I might be able to be talked into a language design that expressly acknowledges that it's just providing documentation and usability hints and doesn't normally let you avoid the need for default cases. But I don't think there's a compelling need for such a feature to land in Swift 5.

I personally disagree with language features that are based on documentation provided. People are sloppy, you can update documentation of one method, but not of the one that calls it, etc.

Alternatives I've considered:

- wrapping all the errors into an "Error" enum which would switch over type of the error, which is not a great solution as in some cases you only throw one type of error

That's interesting. In what cases do you only throw one type of error? Does it not have a catch-all case?

E.g. the initializer will only throw initialization errors - never anything else - it makes no sense to check for sending errors, for example.

- I could throw some error that only contains verbose description of the problem (generally a String), but I don't feel it's the library's job to stringify the error as it can be used for a command-line tools as well

Absolutely. Using enums is a much better way of structuring errors than using a string.

Perhpas I'm missing something, but dealing with UI and presenting an adequate error dialog to the user can be a challenge in current state of things given that currently, in the error catching, you fallback to the basic Error type and generally don't have a lot of options but to display something like "unknown error" - which is terrible for the user.

Again, it comes down to whether that's ever completely avoidable, and I don't think it is. Good error-handling means doing your best to handle common errors well.

This depends on the design, IMHO. Consider the Data structure and what it can throw - completely different types of errors for various methods. Right now, unless you browse the code, you have absolutely no idea what's coming at you. In case of init(contentsOf:), it currently throws something from Cocoa as the init passes is to NSData, but generally, this could be dumbed down to an enum of a few cases - fileDoesNotExist, permissionDenied, ioError, outOfMemory - not sure if more is needed. In any case, when you know what's being thrown and if it's well-defined, it can be largely helpful - if the file doesn't exist, let's create it or show an open dialog. If permission was denied, let's ask for admin privileges (not talking about iOS apps, obviously, but rather macOS command line tool, etc.), if it's IO error, try it again (may be reading from a CD/DVD), if it's out of memory, let's use stream processing, etc.

···

On Aug 18, 2017, at 10:22 AM, John McCall <rjmccall@apple.com> wrote:

On Aug 18, 2017, at 3:28 AM, Charlie Monroe <charlie@charliemonroe.net <mailto:charlie@charliemonroe.net>> wrote:

On Aug 18, 2017, at 8:27 AM, John McCall via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Aug 17, 2017, at 7:39 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

John.

Sometimes we then go on to have a conversation about wrapping errors in other error types, and that can be interesting, but now we're talking about adding a big, messy feature just to get "safety" guarantees for a fairly minor need.

Programmers often have an instinct to obsess over error taxonomies that is very rarely directed at solving any real problem; it is just self-imposed busy-work.

- Some people think that while it might be useful in some narrow cases, the utility isn’t high enough to justify making the language more complex (complexity that would intrude on the APIs of result types, futures, etc)

I’m sure there are other points in the discussion that I’m forgetting.

One thing that I’m personally very concerned about is in the systems programming domain. Systems code is sort of the classic example of code that is low-level enough and finely specified enough that there are lots of knowable things, including the failure modes.

Here we are using "systems" to mean "embedded systems and kernels". And frankly even a kernel is a large enough system that they don't want to exhaustively switch over failures; they just want the static guarantees that go along with a constrained error type.

Beyond expressivity though, our current model involves boxing thrown values into an Error existential, something that forces an implicit memory allocation when the value is large. Unless this is fixed, I’m very concerned that we’ll end up with a situation where certain kinds of systems code (i.e., that which cares about real time guarantees) will not be able to use error handling at all.

JohnMC has some ideas on how to change code generation for ‘throws’ to avoid this problem, but I don’t understand his ideas enough to know if they are practical and likely to happen or not.

Essentially, you give Error a tagged-pointer representation to allow payload-less errors on non-generic error types to be allocated globally, and then you can (1) tell people to not throw errors that require allocation if it's vital to avoid allocation (just like we would tell them today not to construct classes or indirect enum cases) and (2) allow a special global payload-less error to be substituted if error allocation fails.

Of course, we could also say that systems code is required to use a typed-throws feature that we add down the line for their purposes. Or just tell them to not use payloads. Or force them to constrain their error types to fit within some given size. (Note that obsessive error taxonomies tend to end up with a bunch of indirect enum cases anyway, because they get recursive, so the allocation problem is very real whatever we do.)

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

There's no reason we couldn't do some tooling work to expose emergent information about what kinds of errors are thrown by the current implementation of a function, and maybe even check that against the current documentation. Certainly, it should be possible to document particularly interesting errors that a function might throw.

I'd prefer moving some complicated and controversial questions out of the language level, and probably, this might be such a case — but I think the preferred syntax would not be "throws(NetworkError, MyError) func mayFail() throws", but rather something that looks much more integrated ("func mayFail() throws(NetworkError, MyError)")

I'm just challenging the idea that this should be reflected and enforced in the type system.

Yes, I think this might be a can of worms — just imagine rethrows, where some errors will always be caught, so that ultimately, we would steer towards some kind of type algebra just for errors...

Typed throws could also help to lessen the tight coupling to Objective-C:
Being forced to declare conformance to a protocol without requirements or even public methods feels very awkward to me.

Error is not about Objective-C interop; we could make the feature work without a protocol, and in fact the protocol alone isn't sufficient for what we try to do with it. The protocol's value is mostly communicative, a way of making it obvious that a type is meant to be thrown

I guess nearly all uses look like "XYError: Error", so I really don't see much value in the protocol; but to me, it always feels a little bit dirty when language features are entwined with certain types, so it might be just a personal oddity.

If people want to build huge lists of possible errors… why not? As long as I'm still able to write my own throwing functions as today, I'm fine with that.

Language features have to meet a higher bar than "why not?".

That was more geared towards something I'd consider as derailed use — but it seems we agree that typed throws don't have to be something to bother the compiler with.

- Tino

Sent from my iPad

Splitting this off into its own thread:

One related topic that isn’t discussed is type errors. Many third party libraries use a Result type with typed errors. Moving to an async / await model without also introducing typed errors into Swift would require giving up something that is highly valued by many Swift developers. Maybe Swift 5 is the right time to tackle typed errors as well. I would be happy to help with design and drafting a proposal but would need collaborators on the implementation side.

Typed throws is something we need to settle one way or the other, and I agree it would be nice to do that in the Swift 5 cycle.

For the purposes of this sub-discussion, I think there are three kinds of code to think about:
1) large scale API like Cocoa which evolve (adding significant functionality) over the course of many years and can’t break clients.
2) the public API of shared swiftpm packages, whose lifecycle may rise and fall - being obsoleted and replaced by better packages if they encounter a design problem.
3) internal APIs and applications, which are easy to change because the implementations and clients of the APIs are owned by the same people.

These each have different sorts of concerns, and we hope that something can start out as #3 but work its way up the stack gracefully.

Here is where I think things stand on it:
- There is consensus that untyped throws is the right thing for a large scale API like Cocoa. NSError is effectively proven here. Even if typed throws is introduced, Apple is unlikely to adopt it in their APIs for this reason.
- There is consensus that untyped throws is the right default for people to reach for for public package (#2).
- There is consensus that Java and other systems that encourage lists of throws error types lead to problematic APIs for a variety of reasons.
- There is disagreement about whether internal APIs (#3) should use it. It seems perfect to be able to write exhaustive catches in this situation, since everything in knowable. OTOH, this could encourage abuse of error handling in cases where you really should return an enum instead of using throws.
- Some people are concerned that introducing typed throws would cause people to reach for it instead of using untyped throws for public package APIs.

Even for non-public code. The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch. People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws. Every. Single. Time.

I agree that exhaustive switching over errors is something that people are extremely likely to actually want to do. I also think it's a bit of a red herring. The value of typed errors is *not* in exhaustive switching. It is in categorization and verified documentation.

Here is a concrete example that applies to almost every app. When you make a network request there are many things that could go wrong to which you may want to respond differently:
* There might be no network available. You might recover by updating the UI to indicate that and start monitoring for a reachability change.
* There might have been a server error that should eventually be resolved (500). You might update the UI and provide the user the ability to retry.
* There might have been an unrecoverable server error (404). You will update the UI.
* There might have been a low level parsing error (bad JSON, etc). Recovery is perhaps similar in nature to #2, but the problem is less likely to be resolved quickly so you may not provide a retry option. You might also want to do something to notify your dev team that the server is returning JSON that can't be parsed.
* There might have been a higher-level parsing error (converting JSON to model types). This might be treated the same as bad JSON. On the other hand, depending on the specifics of the app, you might take an alternate path that only parses the most essential model data in hopes that the problem was somewhere else and this parse will succeed.

All of this can obviously be accomplished with untyped errors. That said, using types to categorize errors would significantly improve the clarity of such code.

Recall that we're not talking about error values themselves being untyped, i.e. just strings or something. They still have arbitrary typed structure; you can have an enum of network errors that claims to tell you all the ways that an API can fail due to network errors, and that's still extremely informative. It's just that the typing is dynamic at the highest level (and only there, subject to the definition of your error types). You have to ask whether it failed specifically because of a network error, and you have to handle the possibility that it failed for some other reason.

More importantly, I believe that by categorizing errors in ways that are most relevant to a specific domain a library (perhaps internal to an app) can encourage developers to think carefully about how to respond.

Absolutely.

Bad error handling is pervasive. The fact that everyone shows you code that just logs the error is a prime example of this. It should be considered a symptom of a problem, not an acceptable status quo to be maintained. We need all the tools at our disposal to encourage better thinking about and handling of errors. Most importantly, I think we need a middle ground between completely untyped errors and an exhaustive list of every possible error that might happen. I believe a well designed mechanism for categorizing errors in a compiler-verified way can do exactly this.

If that middle ground is just "here's a list of errors that I, as an API writer, want you to specifically be aware of that this might throw", that's essentially just documentation.

In many respects, there are similarities to this in the design of `NSError` which provides categorization via the error domain. This categorization is a bit more broad than I think is useful in many cases, but it is the best example I'm aware of.

The design of NSError is essentially the design of Error. There are arbitrary, extensible categories (error types) which can each be broken down into specific failures (cases of those types), but the type of the standard container itself doesn't restrict what categories might be present.

The primary difference between error domains and the kind of categorization I am proposing is that error domains categorize based on the source of an error whereas I am proposing categorization driven by likely recovery strategies. Recovery is obviously application dependent, but I think the example above demonstrates that there are some useful generalizations that can be made (especially in an app-specific library), even if they don't apply everywhere.

I'm not sure that recoverability is a generic property in that way, but it's an interesting idea.

Sometimes we then go on to have a conversation about wrapping errors in other error types, and that can be interesting, but now we're talking about adding a big, messy feature just to get "safety" guarantees for a fairly minor need.

I think you're right that wrapping errors is tightly related to an effective use of typed errors. You can do a reasonable job without language support (as has been discussed on the list in the past). On the other hand, if we're going to introduce typed errors we should do it in a way that *encourages* effective use of them. My opinion is that encouraging effect use means categorizing (wrapping) errors without requiring any additional syntax beyond the simple `try` used by untyped errors. In practice, this means we should not need to catch and rethrow an error if all we want to do is categorize it. Rust provides good prior art in this area.

Yes, the ability to translate errors between domains is definitely something we could work on, whether we have typed errors or not.

John.

···

On Aug 18, 2017, at 10:19 AM, Matthew Johnson <matthew@anandabits.com> wrote:
On Aug 18, 2017, at 1:27 AM, John McCall <rjmccall@apple.com> wrote:

On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 17, 2017, at 7:39 PM, Matthew Johnson <matthew@anandabits.com> wrote:

Programmers often have an instinct to obsess over error taxonomies that is very rarely directed at solving any real problem; it is just self-imposed busy-work.

I agree that obsessing over intricate taxonomies is counter-productive and should be discouraged. On the other hand, I hope the example I provided above can help to focus the discussion on a practical use of types to categorize errors in a way that helps guide *thinking* and therefore improves error handling in practice.

- Some people think that while it might be useful in some narrow cases, the utility isn’t high enough to justify making the language more complex (complexity that would intrude on the APIs of result types, futures, etc)

I’m sure there are other points in the discussion that I’m forgetting.

One thing that I’m personally very concerned about is in the systems programming domain. Systems code is sort of the classic example of code that is low-level enough and finely specified enough that there are lots of knowable things, including the failure modes.

Here we are using "systems" to mean "embedded systems and kernels". And frankly even a kernel is a large enough system that they don't want to exhaustively switch over failures; they just want the static guarantees that go along with a constrained error type.

Beyond expressivity though, our current model involves boxing thrown values into an Error existential, something that forces an implicit memory allocation when the value is large. Unless this is fixed, I’m very concerned that we’ll end up with a situation where certain kinds of systems code (i.e., that which cares about real time guarantees) will not be able to use error handling at all.

JohnMC has some ideas on how to change code generation for ‘throws’ to avoid this problem, but I don’t understand his ideas enough to know if they are practical and likely to happen or not.

Essentially, you give Error a tagged-pointer representation to allow payload-less errors on non-generic error types to be allocated globally, and then you can (1) tell people to not throw errors that require allocation if it's vital to avoid allocation (just like we would tell them today not to construct classes or indirect enum cases) and (2) allow a special global payload-less error to be substituted if error allocation fails.

Of course, we could also say that systems code is required to use a typed-throws feature that we add down the line for their purposes. Or just tell them to not use payloads. Or force them to constrain their error types to fit within some given size. (Note that obsessive error taxonomies tend to end up with a bunch of indirect enum cases anyway, because they get recursive, so the allocation problem is very real whatever we do.)

John.

Sent from my iPad

>> Splitting this off into its own thread:
>>
>>> One related topic that isn’t discussed is type errors. Many third
party libraries use a Result type with typed errors. Moving to an async /
await model without also introducing typed errors into Swift would require
giving up something that is highly valued by many Swift developers. Maybe
Swift 5 is the right time to tackle typed errors as well. I would be happy
to help with design and drafting a proposal but would need collaborators on
the implementation side.
>>
>> Typed throws is something we need to settle one way or the other, and I
agree it would be nice to do that in the Swift 5 cycle.
>>
>> For the purposes of this sub-discussion, I think there are three kinds
of code to think about:
>> 1) large scale API like Cocoa which evolve (adding significant
functionality) over the course of many years and can’t break clients.
>> 2) the public API of shared swiftpm packages, whose lifecycle may rise
and fall - being obsoleted and replaced by better packages if they
encounter a design problem.
>> 3) internal APIs and applications, which are easy to change because the
implementations and clients of the APIs are owned by the same people.
>>
>> These each have different sorts of concerns, and we hope that something
can start out as #3 but work its way up the stack gracefully.
>>
>> Here is where I think things stand on it:
>> - There is consensus that untyped throws is the right thing for a large
scale API like Cocoa. NSError is effectively proven here. Even if typed
throws is introduced, Apple is unlikely to adopt it in their APIs for this
reason.
>> - There is consensus that untyped throws is the right default for
people to reach for for public package (#2).
>> - There is consensus that Java and other systems that encourage lists
of throws error types lead to problematic APIs for a variety of reasons.
>> - There is disagreement about whether internal APIs (#3) should use
it. It seems perfect to be able to write exhaustive catches in this
situation, since everything in knowable. OTOH, this could encourage abuse
of error handling in cases where you really should return an enum instead
of using throws.
>> - Some people are concerned that introducing typed throws would cause
people to reach for it instead of using untyped throws for public package
APIs.
>
> Even for non-public code. The only practical merit of typed throws I
have ever seen someone demonstrate is that it would let them use contextual
lookup in a throw or catch. People always say "I'll be able to
exhaustively switch over my errors", and then I ask them to show me where
they want to do that, and they show me something that just logs the error,
which of course does not require typed throws. Every. Single. Time.

I agree that exhaustive switching over errors is something that people are
extremely likely to actually want to do. I also think it's a bit of a red
herring. The value of typed errors is *not* in exhaustive switching. It
is in categorization and verified documentation.

Here is a concrete example that applies to almost every app. When you
make a network request there are many things that could go wrong to which
you may want to respond differently:
* There might be no network available. You might recover by updating the
UI to indicate that and start monitoring for a reachability change.
* There might have been a server error that should eventually be resolved
(500). You might update the UI and provide the user the ability to retry.
* There might have been an unrecoverable server error (404). You will
update the UI.
* There might have been a low level parsing error (bad JSON, etc).
Recovery is perhaps similar in nature to #2, but the problem is less likely
to be resolved quickly so you may not provide a retry option. You might
also want to do something to notify your dev team that the server is
returning JSON that can't be parsed.
* There might have been a higher-level parsing error (converting JSON to
model types). This might be treated the same as bad JSON. On the other
hand, depending on the specifics of the app, you might take an alternate
path that only parses the most essential model data in hopes that the
problem was somewhere else and this parse will succeed.

All of this can obviously be accomplished with untyped errors. That said,
using types to categorize errors would significantly improve the clarity of
such code. More importantly, I believe that by categorizing errors in ways
that are most relevant to a specific domain a library (perhaps internal to
an app) can encourage developers to think carefully about how to respond.

I used to be rather in favor of adding typed errors, thinking that it can
only benefit and seemed reasonable. However, given the very interesting
discussion here, I'm inclined to think that what you articulate above is
actually a very good argument _against_ adding typed errors.

If I may simplify, the gist of the argument advanced by Tino, Charlie, and
you is that the primary goal is documentation, and that documentation in
the form of prose is insufficient because it can be unreliable. Therefore,
you want a way for the compiler to enforce said documentation. (The
categorization use case, I think, is well addressed by the protocol-based
design discussed already in this thread.)

However, the compiler itself cannot reward, only punish in the form of
errors or warnings; if exhaustive switching is a red herring and the payoff
for typed errors is correct documentation, the effectiveness of this kind
of compiler enforcement must be directly proportional to the degree of
extrinsic punishment inflicted by the compiler (since the intrinsic reward
of correct documentation is the same whether it's spelled using doc
comments or the type system). This seems like a heavy-handed way to enforce
documentation of only one specific aspect of a throwing function; moreover,
if this use case were to be sufficiently compelling, then it's certainly a
better argument for SourceKit (or some other builtin tool) to automatically
generate information on all errors thrown than for the compiler to require
that users declare it themselves--even if opt-in.

Bad error handling is pervasive. The fact that everyone shows you code

···

On Fri, Aug 18, 2017 at 09:20 Matthew Johnson via swift-evolution < swift-evolution@swift.org> wrote:

On Aug 18, 2017, at 1:27 AM, John McCall <rjmccall@apple.com> wrote:
>> On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution < > swift-evolution@swift.org> wrote:
>>> On Aug 17, 2017, at 7:39 PM, Matthew Johnson <matthew@anandabits.com> > wrote:
that just logs the error is a prime example of this. It should be
considered a symptom of a problem, not an acceptable status quo to be
maintained. We need all the tools at our disposal to encourage better
thinking about and handling of errors. Most importantly, I think we need a
middle ground between completely untyped errors and an exhaustive list of
every possible error that might happen. I believe a well designed
mechanism for categorizing errors in a compiler-verified way can do exactly
this.

In many respects, there are similarities to this in the design of
`NSError` which provides categorization via the error domain. This
categorization is a bit more broad than I think is useful in many cases,
but it is the best example I'm aware of.

The primary difference between error domains and the kind of
categorization I am proposing is that error domains categorize based on the
source of an error whereas I am proposing categorization driven by likely
recovery strategies. Recovery is obviously application dependent, but I
think the example above demonstrates that there are some useful
generalizations that can be made (especially in an app-specific library),
even if they don't apply everywhere.

> Sometimes we then go on to have a conversation about wrapping errors in
other error types, and that can be interesting, but now we're talking about
adding a big, messy feature just to get "safety" guarantees for a fairly
minor need.

I think you're right that wrapping errors is tightly related to an
effective use of typed errors. You can do a reasonable job without
language support (as has been discussed on the list in the past). On the
other hand, if we're going to introduce typed errors we should do it in a
way that *encourages* effective use of them. My opinion is that
encouraging effect use means categorizing (wrapping) errors without
requiring any additional syntax beyond the simple `try` used by untyped
errors. In practice, this means we should not need to catch and rethrow an
error if all we want to do is categorize it. Rust provides good prior art
in this area.

>
> Programmers often have an instinct to obsess over error taxonomies that
is very rarely directed at solving any real problem; it is just
self-imposed busy-work.

I agree that obsessing over intricate taxonomies is counter-productive and
should be discouraged. On the other hand, I hope the example I provided
above can help to focus the discussion on a practical use of types to
categorize errors in a way that helps guide *thinking* and therefore
improves error handling in practice.

>
>> - Some people think that while it might be useful in some narrow cases,
the utility isn’t high enough to justify making the language more complex
(complexity that would intrude on the APIs of result types, futures, etc)
>>
>> I’m sure there are other points in the discussion that I’m forgetting.
>>
>> One thing that I’m personally very concerned about is in the systems
programming domain. Systems code is sort of the classic example of code
that is low-level enough and finely specified enough that there are lots of
knowable things, including the failure modes.
>
> Here we are using "systems" to mean "embedded systems and kernels". And
frankly even a kernel is a large enough system that they don't want to
exhaustively switch over failures; they just want the static guarantees
that go along with a constrained error type.
>
>> Beyond expressivity though, our current model involves boxing thrown
values into an Error existential, something that forces an implicit memory
allocation when the value is large. Unless this is fixed, I’m very
concerned that we’ll end up with a situation where certain kinds of systems
code (i.e., that which cares about real time guarantees) will not be able
to use error handling at all.
>>
>> JohnMC has some ideas on how to change code generation for ‘throws’ to
avoid this problem, but I don’t understand his ideas enough to know if they
are practical and likely to happen or not.
>
> Essentially, you give Error a tagged-pointer representation to allow
payload-less errors on non-generic error types to be allocated globally,
and then you can (1) tell people to not throw errors that require
allocation if it's vital to avoid allocation (just like we would tell them
today not to construct classes or indirect enum cases) and (2) allow a
special global payload-less error to be substituted if error allocation
fails.
>
> Of course, we could also say that systems code is required to use a
typed-throws feature that we add down the line for their purposes. Or just
tell them to not use payloads. Or force them to constrain their error
types to fit within some given size. (Note that obsessive error taxonomies
tend to end up with a bunch of indirect enum cases anyway, because they get
recursive, so the allocation problem is very real whatever we do.)
>
> John.

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

That's how the best C++ unwiding mechanisms work already. I always
thought it was off the table because we were wedded to the idea that
throwing functions are just effectively returning a Result<T> normally
under the covers, but if not, so much the better!

···

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

On Aug 17, 2017, at 11:27 PM, John McCall via swift-evolution >> <swift-evolution@swift.org> wrote:

Essentially, you give Error a tagged-pointer representation to allow
payload-less errors on non-generic error types to be allocated

globally, and then you can (1) tell people to not throw errors that
require allocation if it's vital to avoid allocation (just like we
would tell them today not to construct classes or indirect enum
cases) and (2) allow a special global payload-less error to be
substituted if error allocation fails.

Of course, we could also say that systems code is required to use a
typed-throws feature that we add down the line for their purposes.
Or just tell them to not use payloads. Or force them to constrain
their error types to fit within some given size. (Note that
obsessive error taxonomies tend to end up with a bunch of indirect
enum cases anyway, because they get recursive, so the allocation
problem is very real whatever we do.)

Alternatively, with some LLVM work, we could have the thrower leave
the error value on the stack when propagating an error, and make it
the catcher's responsibility to consume the error value and pop the
stack. We could make not only errors but existential returns in
general more efficient and "systems code"-worthy with a technique like
that.

--
-Dave

The Rust approach of automatically wrapping errors when you "cross domains", so to speak, has the disadvantage you've observed before that the layers of wrapping can obscure the structure of the underlying error when you're trying to ferret out and handle a particular form of failure mode. An alternative approach that embraces the open nature of errors could be to represent domains as independent protocols, and extend the error types that are relevant to that domain to conform to the protocol. That way, you don't obscure the structure of the underlying error value with wrappers. If you expect to exhaustively handle all errors in a domain, well, you'd almost certainly going to need to have a fallback case in your wrapper type for miscellaneous errors, but you could represent that instead without wrapping via a catch-all, and as?-casting to your domain protocol with a ??-default for errors that don't conform to the protocol. For example, instead of attempting something like thi
s:

enum DatabaseError {
  case queryError(QueryError)
  case ioError(IOError)
  case other(Error)

  var errorKind: String {
    switch self {
      case .queryError(let q): return "query error: \(q.query)"
      case .ioError(let i): return "io error: \(i.filename)"
      case .other(let e): return "\(e)"
    }
  }
}

func queryDatabase(_ query: String) throws /*DatabaseError*/ -> Table

do {
  queryDatabase("delete * from users")
} catch let d as DatabaseError {
  os_log(d.errorKind)
} catch {
  fatalError("unexpected non-database error")
}

You could do this:

protocol DatabaseError {
  var errorKind: String { get }
}

extension QueryError: DatabaseError {
  var errorKind: String { return "query error: \(q.query)" }
}
extension IOError: DatabaseError {
  var errorKind: String ( return "io error: \(i.filename)" }
}

extension Error {
  var databaseErrorKind: String {
    return (error as? DatabaseError)?.errorKind ?? "unexpected non-database error"
  }
}

func queryDatabase(_ query: String) throws -> Table

do {
  queryDatabase("delete * from users")
} catch {
  os_log(error.databaseErrorKind)
}

-Joe

···

On Aug 18, 2017, at 11:09 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:

I think you're right that wrapping errors is tightly related to an effective use of typed errors. You can do a reasonable job without language support (as has been discussed on the list in the past). On the other hand, if we're going to introduce typed errors we should do it in a way that *encourages* effective use of them. My opinion is that encouraging effect use means categorizing (wrapping) errors without requiring any additional syntax beyond the simple `try` used by untyped errors. In practice, this means we should not need to catch and rethrow an error if all we want to do is categorize it. Rust provides good prior art in this area.

Yes, the ability to translate errors between domains is definitely something we could work on, whether we have typed errors or not.

Sent from my iPad

Splitting this off into its own thread:

One related topic that isn’t discussed is type errors. Many third party libraries use a Result type with typed errors. Moving to an async / await model without also introducing typed errors into Swift would require giving up something that is highly valued by many Swift developers. Maybe Swift 5 is the right time to tackle typed errors as well. I would be happy to help with design and drafting a proposal but would need collaborators on the implementation side.

Typed throws is something we need to settle one way or the other, and I agree it would be nice to do that in the Swift 5 cycle.

For the purposes of this sub-discussion, I think there are three kinds of code to think about:
1) large scale API like Cocoa which evolve (adding significant functionality) over the course of many years and can’t break clients.
2) the public API of shared swiftpm packages, whose lifecycle may rise and fall - being obsoleted and replaced by better packages if they encounter a design problem.
3) internal APIs and applications, which are easy to change because the implementations and clients of the APIs are owned by the same people.

These each have different sorts of concerns, and we hope that something can start out as #3 but work its way up the stack gracefully.

Here is where I think things stand on it:
- There is consensus that untyped throws is the right thing for a large scale API like Cocoa. NSError is effectively proven here. Even if typed throws is introduced, Apple is unlikely to adopt it in their APIs for this reason.
- There is consensus that untyped throws is the right default for people to reach for for public package (#2).
- There is consensus that Java and other systems that encourage lists of throws error types lead to problematic APIs for a variety of reasons.
- There is disagreement about whether internal APIs (#3) should use it. It seems perfect to be able to write exhaustive catches in this situation, since everything in knowable. OTOH, this could encourage abuse of error handling in cases where you really should return an enum instead of using throws.
- Some people are concerned that introducing typed throws would cause people to reach for it instead of using untyped throws for public package APIs.

Even for non-public code. The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch. People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws. Every. Single. Time.

I agree that exhaustive switching over errors is something that people are extremely likely to actually want to do. I also think it's a bit of a red herring. The value of typed errors is *not* in exhaustive switching. It is in categorization and verified documentation.

Here is a concrete example that applies to almost every app. When you make a network request there are many things that could go wrong to which you may want to respond differently:
* There might be no network available. You might recover by updating the UI to indicate that and start monitoring for a reachability change.
* There might have been a server error that should eventually be resolved (500). You might update the UI and provide the user the ability to retry.
* There might have been an unrecoverable server error (404). You will update the UI.
* There might have been a low level parsing error (bad JSON, etc). Recovery is perhaps similar in nature to #2, but the problem is less likely to be resolved quickly so you may not provide a retry option. You might also want to do something to notify your dev team that the server is returning JSON that can't be parsed.
* There might have been a higher-level parsing error (converting JSON to model types). This might be treated the same as bad JSON. On the other hand, depending on the specifics of the app, you might take an alternate path that only parses the most essential model data in hopes that the problem was somewhere else and this parse will succeed.

All of this can obviously be accomplished with untyped errors. That said, using types to categorize errors would significantly improve the clarity of such code.

Recall that we're not talking about error values themselves being untyped, i.e. just strings or something. They still have arbitrary typed structure; you can have an enum of network errors that claims to tell you all the ways that an API can fail due to network errors, and that's still extremely informative. It's just that the typing is dynamic at the highest level (and only there, subject to the definition of your error types). You have to ask whether it failed specifically because of a network error, and you have to handle the possibility that it failed for some other reason.

More importantly, I believe that by categorizing errors in ways that are most relevant to a specific domain a library (perhaps internal to an app) can encourage developers to think carefully about how to respond.

Absolutely.

Bad error handling is pervasive. The fact that everyone shows you code that just logs the error is a prime example of this. It should be considered a symptom of a problem, not an acceptable status quo to be maintained. We need all the tools at our disposal to encourage better thinking about and handling of errors. Most importantly, I think we need a middle ground between completely untyped errors and an exhaustive list of every possible error that might happen. I believe a well designed mechanism for categorizing errors in a compiler-verified way can do exactly this.

If that middle ground is just "here's a list of errors that I, as an API writer, want you to specifically be aware of that this might throw", that's essentially just documentation.

In many respects, there are similarities to this in the design of `NSError` which provides categorization via the error domain. This categorization is a bit more broad than I think is useful in many cases, but it is the best example I'm aware of.

The design of NSError is essentially the design of Error. There are arbitrary, extensible categories (error types) which can each be broken down into specific failures (cases of those types), but the type of the standard container itself doesn't restrict what categories might be present.

The primary difference between error domains and the kind of categorization I am proposing is that error domains categorize based on the source of an error whereas I am proposing categorization driven by likely recovery strategies. Recovery is obviously application dependent, but I think the example above demonstrates that there are some useful generalizations that can be made (especially in an app-specific library), even if they don't apply everywhere.

I'm not sure that recoverability is a generic property in that way, but it's an interesting idea.

It’s not *completely* general, but within a bounded domain it is certainly possible to make general decisions about how you intend to recover in the majority of specific cases.

Sometimes we then go on to have a conversation about wrapping errors in other error types, and that can be interesting, but now we're talking about adding a big, messy feature just to get "safety" guarantees for a fairly minor need.

I think you're right that wrapping errors is tightly related to an effective use of typed errors. You can do a reasonable job without language support (as has been discussed on the list in the past). On the other hand, if we're going to introduce typed errors we should do it in a way that *encourages* effective use of them. My opinion is that encouraging effect use means categorizing (wrapping) errors without requiring any additional syntax beyond the simple `try` used by untyped errors. In practice, this means we should not need to catch and rethrow an error if all we want to do is categorize it. Rust provides good prior art in this area.

Yes, the ability to translate errors between domains is definitely something we could work on, whether we have typed errors or not.

I’m glad to hear that! :) To be clear, I’m not stuck on typed errors as the only way to solve this problem, but do have specific use cases in mind that I would like to see addressed one way or another.

The primary goal for me personally is to factor out and centralize code that categorizes an error, allowing catch sites to focus on implementing recovery instead of figuring out what went wrong. Here’s some concrete sample code building on the example scenario above:

enum NetworkError: Error {
   case noNetwork(Error, URLResponse)
   case serverFailure(Error, URLResponse)
   case badRequest(Error, URLResponse)
   case authentication(Error, URLResponse)

   var response: URLResponse { // switch }
   var underlyingError: Error { // switch }

   init(error: Error, response: URLResponse) {
      // inspect error and response and categorize appropriately
   }
}

enum ParsingError: Error {
   case deserialization(Error, Data)
   case decoding(Error, Data)

   var data: Data { // switch }
   var underlyingError: Error { // switch }

   init(error: Error, data: Data) {
      // inspect error and response and categorize appropriately
      // this assumes errors reported by the decoder provide sufficient information to distinguish the errors as desired
      // prior to Codable the first case would come from NSJSONSerialization and the other from the dictionary to model conversion
   }
}

enum NestedAPIRequestError: Error {
    case network(NetworkError)
    case parsing(ParsingError)

   var underlyingError: Error { // switch }
}

— or

enum FlatAPIRequestError: Error {
   case noNetwork(Error, URLResponse)
   case serverFailure(Error, URLResponse)
   case badRequest(Error, URLResponse)
   case authentication(Error, URLResponse)
   case deserialization(Error, Data)
   case decoding(Error, Data)

   var underlyingError: Error { // switch }
}

The important thing to note is that the categorization algorithms are driven by the intended recovery strategy and relies on knowledge about the domain. The algorithm itself is selected by the dynamic context as the error propagates. In some cases the underlying error itself is sufficient for categorization, but in other cases contextual data is also used (notably the URLResponse). I recall looking into a Boost error handling library Dave Abrahams pointed me to (early this year?) whose intent was to attach dynamic data to an error as it flows up the stack. That was a very interesting idea and seems especially powerful when coupled with this kind of error categorization.

It may or may not be worthwhile to have the ability to determine that “other” / “unknown” errors fall into one of the categories. It is certainly the case that many contexts will require an “other" / “unknown” category. I wouldn’t feel too bad about a solution that always included an unbounded case, at least in async code. That would still be a huge step forward from where we are now. On the other hand, there are cases where we are in full control of all errors that are thrown and can ensure completeness of the categorization. Most notably, it could be handy to have exhaustive error categorization when using (abusing?) error handling for control flow in things like parsers, test code, etc.

Stepping away from specific solutions, what I am personally looking for is the following:

1. A way to factor out these categorization algorithms.
2. A way to have them invoked without boilerplate as errors propagate.
3. A way to have documentation in the source code that is compiler verified regarding the categorization in effect at a function boundary.
4. A way to match errors based on this categorization (included nested categorization in the case of NestedAPIRequestError).

If we come up with a way to address these goals without using the type system I’m sure I will be happy! :) Types just feel like the most natural / obvious solution.

With the above in mind, what are your thoughts on the four goals outlined above? Do you have any thoughts on how they might be addressed in a way that doesn’t use the type system?

Matthew

···

On Aug 18, 2017, at 1:09 PM, John McCall <rjmccall@apple.com> wrote:

On Aug 18, 2017, at 10:19 AM, Matthew Johnson <matthew@anandabits.com> wrote:
On Aug 18, 2017, at 1:27 AM, John McCall <rjmccall@apple.com> wrote:

On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 17, 2017, at 7:39 PM, Matthew Johnson <matthew@anandabits.com> wrote:

John.

Programmers often have an instinct to obsess over error taxonomies that is very rarely directed at solving any real problem; it is just self-imposed busy-work.

I agree that obsessing over intricate taxonomies is counter-productive and should be discouraged. On the other hand, I hope the example I provided above can help to focus the discussion on a practical use of types to categorize errors in a way that helps guide *thinking* and therefore improves error handling in practice.

- Some people think that while it might be useful in some narrow cases, the utility isn’t high enough to justify making the language more complex (complexity that would intrude on the APIs of result types, futures, etc)

I’m sure there are other points in the discussion that I’m forgetting.

One thing that I’m personally very concerned about is in the systems programming domain. Systems code is sort of the classic example of code that is low-level enough and finely specified enough that there are lots of knowable things, including the failure modes.

Here we are using "systems" to mean "embedded systems and kernels". And frankly even a kernel is a large enough system that they don't want to exhaustively switch over failures; they just want the static guarantees that go along with a constrained error type.

Beyond expressivity though, our current model involves boxing thrown values into an Error existential, something that forces an implicit memory allocation when the value is large. Unless this is fixed, I’m very concerned that we’ll end up with a situation where certain kinds of systems code (i.e., that which cares about real time guarantees) will not be able to use error handling at all.

JohnMC has some ideas on how to change code generation for ‘throws’ to avoid this problem, but I don’t understand his ideas enough to know if they are practical and likely to happen or not.

Essentially, you give Error a tagged-pointer representation to allow payload-less errors on non-generic error types to be allocated globally, and then you can (1) tell people to not throw errors that require allocation if it's vital to avoid allocation (just like we would tell them today not to construct classes or indirect enum cases) and (2) allow a special global payload-less error to be substituted if error allocation fails.

Of course, we could also say that systems code is required to use a typed-throws feature that we add down the line for their purposes. Or just tell them to not use payloads. Or force them to constrain their error types to fit within some given size. (Note that obsessive error taxonomies tend to end up with a bunch of indirect enum cases anyway, because they get recursive, so the allocation problem is very real whatever we do.)

John.

Hello all,

I'm also on the "side" of untyped errors, but I can imagine how other developers may like a stricter error hierarchy. It surely fits some situations.

Enter Result<T> and Result<T, E>:

Since Swift "native" errors don't fit well with asynchronous APIs, various ways to encapsulate them have emerged, most of them eventually relying on some kind of variant of those `Result` type:

  // Untyped errors
  enum Result<T> {
    case success(T)
    case failure(Error)
  }
  
  // Typed errors
  enum Result<T, E: Error> {
    case success(T)
    case failure(E)
  }

The first Result<T> fits well people who like untyped errors. And Result<T, E> fits people who prefer typed errors. Result<T> is objectively closer to the "spirit" of Swift 2-4. Yet Result<T, E> has the right to live as well.

When Swift 5 brings sugar syntax around async/await/etc, most needs for Result<T> will naturally vanish.

However, the need for Result<T, E> will remain. The debate about "typed throws", for me, sums up to this question: will the typed folks be able to take profit from the syntax sugar brought by async/await/etc of Swift 5? Or will they have to keep on carrying Result<T, E> with them?

If I understand correctly, the people really attached to Result<T,E> often use it as a normal result type rather than using the built-in error machinery at all. That is, of course, their right. If they're doing that, then they can have an async function that does that just as well as they can have a non-async function. However, "async" implying "throws" would undermine them to a significant extent.

John.

···

On Aug 18, 2017, at 4:40 AM, Gwendal Roué <gwendal.roue@gmail.com> wrote:

Gwendal Roué

Le 18 août 2017 à 10:23, John McCall via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> a écrit :

On Aug 18, 2017, at 3:28 AM, Charlie Monroe <charlie@charliemonroe.net <mailto:charlie@charliemonroe.net>> wrote:

On Aug 18, 2017, at 8:27 AM, John McCall via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
Splitting this off into its own thread:

On Aug 17, 2017, at 7:39 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:
One related topic that isn’t discussed is type errors. Many third party libraries use a Result type with typed errors. Moving to an async / await model without also introducing typed errors into Swift would require giving up something that is highly valued by many Swift developers. Maybe Swift 5 is the right time to tackle typed errors as well. I would be happy to help with design and drafting a proposal but would need collaborators on the implementation side.

Typed throws is something we need to settle one way or the other, and I agree it would be nice to do that in the Swift 5 cycle.

For the purposes of this sub-discussion, I think there are three kinds of code to think about:
1) large scale API like Cocoa which evolve (adding significant functionality) over the course of many years and can’t break clients.
2) the public API of shared swiftpm packages, whose lifecycle may rise and fall - being obsoleted and replaced by better packages if they encounter a design problem.
3) internal APIs and applications, which are easy to change because the implementations and clients of the APIs are owned by the same people.

These each have different sorts of concerns, and we hope that something can start out as #3 but work its way up the stack gracefully.

Here is where I think things stand on it:
- There is consensus that untyped throws is the right thing for a large scale API like Cocoa. NSError is effectively proven here. Even if typed throws is introduced, Apple is unlikely to adopt it in their APIs for this reason.
- There is consensus that untyped throws is the right default for people to reach for for public package (#2).
- There is consensus that Java and other systems that encourage lists of throws error types lead to problematic APIs for a variety of reasons.
- There is disagreement about whether internal APIs (#3) should use it. It seems perfect to be able to write exhaustive catches in this situation, since everything in knowable. OTOH, this could encourage abuse of error handling in cases where you really should return an enum instead of using throws.
- Some people are concerned that introducing typed throws would cause people to reach for it instead of using untyped throws for public package APIs.

Even for non-public code. The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch. People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws. Every. Single. Time.

The issue I see here with non-typed errors is that relying on documentation is very error-prone. I'll give an example where I've used exhaustive error catching (but then again, I was generally the only one using exhaustive enum switches when we discussed those). I've made a simple library for reporting purchases to a server. The report needs to be signed using a certificate and there are some validations to be made.

This generally divides the errors into three logical areas - initialization (e.g. errors when loading the certificate, etc.), validation (when the document doesn't pass validation) and sending (network error, error response from the server, etc.).

Instead of using a large error enum, I've split this into three enums. At this point, especially for a newcommer to the code, he may not realize which method can throw which of these error enums.

I've found that the app can take advantage of knowing what's wrong. For example, if some required information is missing e.g. Validation.subjectNameMissing is thrown. In such case the application can inform the user that name is missing and it can offer to open UI to enter this information (in the case of my app, the UI for sending is in the document view, while the mentioned "subject name" information is in Preferences).

This way I exhaustively switch over the error enums, suggesting to the user solution of the particular problem without dumbing down to a message "Oops, something went wrong, but I have no idea what because this kind of error is not handled.".

Surely you must have a message like that. You're transmitting over a network, so all sorts of things can go wrong that you're not going to explain in detail to the user or have specific recoveries for. I would guess that have a generic handler for errors, and it has carefully-considered responses for specific failures (validation errors, maybe initialization errors) but a default response for others. Maybe you've put effort into handling more errors intelligently, trying to let fewer and fewer things end up with the default response — that's great, but it must still be there.

That's one of the keys to my argument here: practically speaking, from the perspective of any specific bit of code, there will always be a default response, because errors naturally quickly tend towards complexity, far more complexity than any client can exhaustively handle. Typed throws just means that error types will all have catch-all cases like MyError.other(Error), which mostly seems counter-productive to me.

I totally agree that we could do a lot more for documentation. I might be able to be talked into a language design that expressly acknowledges that it's just providing documentation and usability hints and doesn't normally let you avoid the need for default cases. But I don't think there's a compelling need for such a feature to land in Swift 5.

Alternatives I've considered:

- wrapping all the errors into an "Error" enum which would switch over type of the error, which is not a great solution as in some cases you only throw one type of error

That's interesting. In what cases do you only throw one type of error? Does it not have a catch-all case?

- I could throw some error that only contains verbose description of the problem (generally a String), but I don't feel it's the library's job to stringify the error as it can be used for a command-line tools as well

Absolutely. Using enums is a much better way of structuring errors than using a string.

Perhpas I'm missing something, but dealing with UI and presenting an adequate error dialog to the user can be a challenge in current state of things given that currently, in the error catching, you fallback to the basic Error type and generally don't have a lot of options but to display something like "unknown error" - which is terrible for the user.

Again, it comes down to whether that's ever completely avoidable, and I don't think it is. Good error-handling means doing your best to handle common errors well.

John.

Sometimes we then go on to have a conversation about wrapping errors in other error types, and that can be interesting, but now we're talking about adding a big, messy feature just to get "safety" guarantees for a fairly minor need.

Programmers often have an instinct to obsess over error taxonomies that is very rarely directed at solving any real problem; it is just self-imposed busy-work.

- Some people think that while it might be useful in some narrow cases, the utility isn’t high enough to justify making the language more complex (complexity that would intrude on the APIs of result types, futures, etc)

I’m sure there are other points in the discussion that I’m forgetting.

One thing that I’m personally very concerned about is in the systems programming domain. Systems code is sort of the classic example of code that is low-level enough and finely specified enough that there are lots of knowable things, including the failure modes.

Here we are using "systems" to mean "embedded systems and kernels". And frankly even a kernel is a large enough system that they don't want to exhaustively switch over failures; they just want the static guarantees that go along with a constrained error type.

Beyond expressivity though, our current model involves boxing thrown values into an Error existential, something that forces an implicit memory allocation when the value is large. Unless this is fixed, I’m very concerned that we’ll end up with a situation where certain kinds of systems code (i.e., that which cares about real time guarantees) will not be able to use error handling at all.

JohnMC has some ideas on how to change code generation for ‘throws’ to avoid this problem, but I don’t understand his ideas enough to know if they are practical and likely to happen or not.

Essentially, you give Error a tagged-pointer representation to allow payload-less errors on non-generic error types to be allocated globally, and then you can (1) tell people to not throw errors that require allocation if it's vital to avoid allocation (just like we would tell them today not to construct classes or indirect enum cases) and (2) allow a special global payload-less error to be substituted if error allocation fails.

Of course, we could also say that systems code is required to use a typed-throws feature that we add down the line for their purposes. Or just tell them to not use payloads. Or force them to constrain their error types to fit within some given size. (Note that obsessive error taxonomies tend to end up with a bunch of indirect enum cases anyway, because they get recursive, so the allocation problem is very real whatever we do.)

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

Sent from my iPad

>> Splitting this off into its own thread:
>>
>>> One related topic that isn’t discussed is type errors. Many third party libraries use a Result type with typed errors. Moving to an async / await model without also introducing typed errors into Swift would require giving up something that is highly valued by many Swift developers. Maybe Swift 5 is the right time to tackle typed errors as well. I would be happy to help with design and drafting a proposal but would need collaborators on the implementation side.
>>
>> Typed throws is something we need to settle one way or the other, and I agree it would be nice to do that in the Swift 5 cycle.
>>
>> For the purposes of this sub-discussion, I think there are three kinds of code to think about:
>> 1) large scale API like Cocoa which evolve (adding significant functionality) over the course of many years and can’t break clients.
>> 2) the public API of shared swiftpm packages, whose lifecycle may rise and fall - being obsoleted and replaced by better packages if they encounter a design problem.
>> 3) internal APIs and applications, which are easy to change because the implementations and clients of the APIs are owned by the same people.
>>
>> These each have different sorts of concerns, and we hope that something can start out as #3 but work its way up the stack gracefully.
>>
>> Here is where I think things stand on it:
>> - There is consensus that untyped throws is the right thing for a large scale API like Cocoa. NSError is effectively proven here. Even if typed throws is introduced, Apple is unlikely to adopt it in their APIs for this reason.
>> - There is consensus that untyped throws is the right default for people to reach for for public package (#2).
>> - There is consensus that Java and other systems that encourage lists of throws error types lead to problematic APIs for a variety of reasons.
>> - There is disagreement about whether internal APIs (#3) should use it. It seems perfect to be able to write exhaustive catches in this situation, since everything in knowable. OTOH, this could encourage abuse of error handling in cases where you really should return an enum instead of using throws.
>> - Some people are concerned that introducing typed throws would cause people to reach for it instead of using untyped throws for public package APIs.
>
> Even for non-public code. The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch. People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws. Every. Single. Time.

I agree that exhaustive switching over errors is something that people are extremely likely to actually want to do. I also think it's a bit of a red herring. The value of typed errors is *not* in exhaustive switching. It is in categorization and verified documentation.

Here is a concrete example that applies to almost every app. When you make a network request there are many things that could go wrong to which you may want to respond differently:
* There might be no network available. You might recover by updating the UI to indicate that and start monitoring for a reachability change.
* There might have been a server error that should eventually be resolved (500). You might update the UI and provide the user the ability to retry.
* There might have been an unrecoverable server error (404). You will update the UI.
* There might have been a low level parsing error (bad JSON, etc). Recovery is perhaps similar in nature to #2, but the problem is less likely to be resolved quickly so you may not provide a retry option. You might also want to do something to notify your dev team that the server is returning JSON that can't be parsed.
* There might have been a higher-level parsing error (converting JSON to model types). This might be treated the same as bad JSON. On the other hand, depending on the specifics of the app, you might take an alternate path that only parses the most essential model data in hopes that the problem was somewhere else and this parse will succeed.

All of this can obviously be accomplished with untyped errors. That said, using types to categorize errors would significantly improve the clarity of such code. More importantly, I believe that by categorizing errors in ways that are most relevant to a specific domain a library (perhaps internal to an app) can encourage developers to think carefully about how to respond.

I used to be rather in favor of adding typed errors, thinking that it can only benefit and seemed reasonable. However, given the very interesting discussion here, I'm inclined to think that what you articulate above is actually a very good argument _against_ adding typed errors.

If I may simplify, the gist of the argument advanced by Tino, Charlie, and you is that the primary goal is documentation, and that documentation in the form of prose is insufficient because it can be unreliable. Therefore, you want a way for the compiler to enforce said documentation. (The categorization use case, I think, is well addressed by the protocol-based design discussed already in this thread.)

Actually documentation is only one of the goals I have and it is the least important. Please see my subsequent reply to John where I articulate the four primary goals I have for improved error handling, whether it be typed errors or some other mechanism. I am curious to see what you think of the goals, as well as what mechanism might best address those goals.

···

On Aug 18, 2017, at 6:15 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Fri, Aug 18, 2017 at 09:20 Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
On Aug 18, 2017, at 1:27 AM, John McCall <rjmccall@apple.com <mailto:rjmccall@apple.com>> wrote:
>> On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>>> On Aug 17, 2017, at 7:39 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

However, the compiler itself cannot reward, only punish in the form of errors or warnings; if exhaustive switching is a red herring and the payoff for typed errors is correct documentation, the effectiveness of this kind of compiler enforcement must be directly proportional to the degree of extrinsic punishment inflicted by the compiler (since the intrinsic reward of correct documentation is the same whether it's spelled using doc comments or the type system). This seems like a heavy-handed way to enforce documentation of only one specific aspect of a throwing function; moreover, if this use case were to be sufficiently compelling, then it's certainly a better argument for SourceKit (or some other builtin tool) to automatically generate information on all errors thrown than for the compiler to require that users declare it themselves--even if opt-in.

Bad error handling is pervasive. The fact that everyone shows you code that just logs the error is a prime example of this. It should be considered a symptom of a problem, not an acceptable status quo to be maintained. We need all the tools at our disposal to encourage better thinking about and handling of errors. Most importantly, I think we need a middle ground between completely untyped errors and an exhaustive list of every possible error that might happen. I believe a well designed mechanism for categorizing errors in a compiler-verified way can do exactly this.

In many respects, there are similarities to this in the design of `NSError` which provides categorization via the error domain. This categorization is a bit more broad than I think is useful in many cases, but it is the best example I'm aware of.

The primary difference between error domains and the kind of categorization I am proposing is that error domains categorize based on the source of an error whereas I am proposing categorization driven by likely recovery strategies. Recovery is obviously application dependent, but I think the example above demonstrates that there are some useful generalizations that can be made (especially in an app-specific library), even if they don't apply everywhere.

> Sometimes we then go on to have a conversation about wrapping errors in other error types, and that can be interesting, but now we're talking about adding a big, messy feature just to get "safety" guarantees for a fairly minor need.

I think you're right that wrapping errors is tightly related to an effective use of typed errors. You can do a reasonable job without language support (as has been discussed on the list in the past). On the other hand, if we're going to introduce typed errors we should do it in a way that *encourages* effective use of them. My opinion is that encouraging effect use means categorizing (wrapping) errors without requiring any additional syntax beyond the simple `try` used by untyped errors. In practice, this means we should not need to catch and rethrow an error if all we want to do is categorize it. Rust provides good prior art in this area.

>
> Programmers often have an instinct to obsess over error taxonomies that is very rarely directed at solving any real problem; it is just self-imposed busy-work.

I agree that obsessing over intricate taxonomies is counter-productive and should be discouraged. On the other hand, I hope the example I provided above can help to focus the discussion on a practical use of types to categorize errors in a way that helps guide *thinking* and therefore improves error handling in practice.

>
>> - Some people think that while it might be useful in some narrow cases, the utility isn’t high enough to justify making the language more complex (complexity that would intrude on the APIs of result types, futures, etc)
>>
>> I’m sure there are other points in the discussion that I’m forgetting.
>>
>> One thing that I’m personally very concerned about is in the systems programming domain. Systems code is sort of the classic example of code that is low-level enough and finely specified enough that there are lots of knowable things, including the failure modes.
>
> Here we are using "systems" to mean "embedded systems and kernels". And frankly even a kernel is a large enough system that they don't want to exhaustively switch over failures; they just want the static guarantees that go along with a constrained error type.
>
>> Beyond expressivity though, our current model involves boxing thrown values into an Error existential, something that forces an implicit memory allocation when the value is large. Unless this is fixed, I’m very concerned that we’ll end up with a situation where certain kinds of systems code (i.e., that which cares about real time guarantees) will not be able to use error handling at all.
>>
>> JohnMC has some ideas on how to change code generation for ‘throws’ to avoid this problem, but I don’t understand his ideas enough to know if they are practical and likely to happen or not.
>
> Essentially, you give Error a tagged-pointer representation to allow payload-less errors on non-generic error types to be allocated globally, and then you can (1) tell people to not throw errors that require allocation if it's vital to avoid allocation (just like we would tell them today not to construct classes or indirect enum cases) and (2) allow a special global payload-less error to be substituted if error allocation fails.
>
> Of course, we could also say that systems code is required to use a typed-throws feature that we add down the line for their purposes. Or just tell them to not use payloads. Or force them to constrain their error types to fit within some given size. (Note that obsessive error taxonomies tend to end up with a bunch of indirect enum cases anyway, because they get recursive, so the allocation problem is very real whatever we do.)
>
> John.

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

Essentially, you give Error a tagged-pointer representation to allow
payload-less errors on non-generic error types to be allocated

globally, and then you can (1) tell people to not throw errors that
require allocation if it's vital to avoid allocation (just like we
would tell them today not to construct classes or indirect enum
cases) and (2) allow a special global payload-less error to be
substituted if error allocation fails.

Of course, we could also say that systems code is required to use a
typed-throws feature that we add down the line for their purposes.
Or just tell them to not use payloads. Or force them to constrain
their error types to fit within some given size. (Note that
obsessive error taxonomies tend to end up with a bunch of indirect
enum cases anyway, because they get recursive, so the allocation
problem is very real whatever we do.)

Alternatively, with some LLVM work, we could have the thrower leave
the error value on the stack when propagating an error, and make it
the catcher's responsibility to consume the error value and pop the
stack. We could make not only errors but existential returns in
general more efficient and "systems code"-worthy with a technique like
that.

That's how the best C++ unwiding mechanisms work already.

Them's fighting words. :)

John.

···

On Aug 25, 2017, at 12:18 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:
on Fri Aug 18 2017, Joe Groff <swift-evolution@swift.org> wrote:

On Aug 17, 2017, at 11:27 PM, John McCall via swift-evolution >>> <swift-evolution@swift.org> wrote:

I always thought it was off the table because we were wedded to the idea that
throwing functions are just effectively returning a Result<T> normally
under the covers, but if not, so much the better!

--
-Dave

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

Sent from my iPad

>> Splitting this off into its own thread:
>>
>>> One related topic that isn’t discussed is type errors. Many third
party libraries use a Result type with typed errors. Moving to an async /
await model without also introducing typed errors into Swift would require
giving up something that is highly valued by many Swift developers. Maybe
Swift 5 is the right time to tackle typed errors as well. I would be happy
to help with design and drafting a proposal but would need collaborators on
the implementation side.
>>
>> Typed throws is something we need to settle one way or the other, and
I agree it would be nice to do that in the Swift 5 cycle.
>>
>> For the purposes of this sub-discussion, I think there are three kinds
of code to think about:
>> 1) large scale API like Cocoa which evolve (adding significant
functionality) over the course of many years and can’t break clients.
>> 2) the public API of shared swiftpm packages, whose lifecycle may rise
and fall - being obsoleted and replaced by better packages if they
encounter a design problem.
>> 3) internal APIs and applications, which are easy to change because
the implementations and clients of the APIs are owned by the same people.
>>
>> These each have different sorts of concerns, and we hope that
something can start out as #3 but work its way up the stack gracefully.
>>
>> Here is where I think things stand on it:
>> - There is consensus that untyped throws is the right thing for a
large scale API like Cocoa. NSError is effectively proven here. Even if
typed throws is introduced, Apple is unlikely to adopt it in their APIs for
this reason.
>> - There is consensus that untyped throws is the right default for
people to reach for for public package (#2).
>> - There is consensus that Java and other systems that encourage lists
of throws error types lead to problematic APIs for a variety of reasons.
>> - There is disagreement about whether internal APIs (#3) should use
it. It seems perfect to be able to write exhaustive catches in this
situation, since everything in knowable. OTOH, this could encourage abuse
of error handling in cases where you really should return an enum instead
of using throws.
>> - Some people are concerned that introducing typed throws would cause
people to reach for it instead of using untyped throws for public package
APIs.
>
> Even for non-public code. The only practical merit of typed throws I
have ever seen someone demonstrate is that it would let them use contextual
lookup in a throw or catch. People always say "I'll be able to
exhaustively switch over my errors", and then I ask them to show me where
they want to do that, and they show me something that just logs the error,
which of course does not require typed throws. Every. Single. Time.

I agree that exhaustive switching over errors is something that people
are extremely likely to actually want to do. I also think it's a bit of a
red herring. The value of typed errors is *not* in exhaustive switching.
It is in categorization and verified documentation.

Here is a concrete example that applies to almost every app. When you
make a network request there are many things that could go wrong to which
you may want to respond differently:
* There might be no network available. You might recover by updating the
UI to indicate that and start monitoring for a reachability change.
* There might have been a server error that should eventually be resolved
(500). You might update the UI and provide the user the ability to retry.
* There might have been an unrecoverable server error (404). You will
update the UI.
* There might have been a low level parsing error (bad JSON, etc).
Recovery is perhaps similar in nature to #2, but the problem is less likely
to be resolved quickly so you may not provide a retry option. You might
also want to do something to notify your dev team that the server is
returning JSON that can't be parsed.
* There might have been a higher-level parsing error (converting JSON to
model types). This might be treated the same as bad JSON. On the other
hand, depending on the specifics of the app, you might take an alternate
path that only parses the most essential model data in hopes that the
problem was somewhere else and this parse will succeed.

All of this can obviously be accomplished with untyped errors. That
said, using types to categorize errors would significantly improve the
clarity of such code. More importantly, I believe that by categorizing
errors in ways that are most relevant to a specific domain a library
(perhaps internal to an app) can encourage developers to think carefully
about how to respond.

I used to be rather in favor of adding typed errors, thinking that it can
only benefit and seemed reasonable. However, given the very interesting
discussion here, I'm inclined to think that what you articulate above is
actually a very good argument _against_ adding typed errors.

If I may simplify, the gist of the argument advanced by Tino, Charlie, and
you is that the primary goal is documentation, and that documentation in
the form of prose is insufficient because it can be unreliable. Therefore,
you want a way for the compiler to enforce said documentation. (The
categorization use case, I think, is well addressed by the protocol-based
design discussed already in this thread.)

Actually documentation is only one of the goals I have and it is the least
important. Please see my subsequent reply to John where I articulate the
four primary goals I have for improved error handling, whether it be typed
errors or some other mechanism. I am curious to see what you think of the
goals, as well as what mechanism might best address those goals.

Your other three goals have to do with what you term categorization, unless
I misunderstand. Are those not adequately addressed by Joe Groff's
protocol-based design?

···

On Fri, Aug 18, 2017 at 6:19 PM, Matthew Johnson <matthew@anandabits.com> wrote:

On Aug 18, 2017, at 6:15 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Fri, Aug 18, 2017 at 09:20 Matthew Johnson via swift-evolution < > swift-evolution@swift.org> wrote:

On Aug 18, 2017, at 1:27 AM, John McCall <rjmccall@apple.com> wrote:
>> On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution < >> swift-evolution@swift.org> wrote:
>>> On Aug 17, 2017, at 7:39 PM, Matthew Johnson <matthew@anandabits.com> >> wrote:

However, the compiler itself cannot reward, only punish in the form of
errors or warnings; if exhaustive switching is a red herring and the payoff
for typed errors is correct documentation, the effectiveness of this kind
of compiler enforcement must be directly proportional to the degree of
extrinsic punishment inflicted by the compiler (since the intrinsic reward
of correct documentation is the same whether it's spelled using doc
comments or the type system). This seems like a heavy-handed way to enforce
documentation of only one specific aspect of a throwing function; moreover,
if this use case were to be sufficiently compelling, then it's certainly a
better argument for SourceKit (or some other builtin tool) to automatically
generate information on all errors thrown than for the compiler to require
that users declare it themselves--even if opt-in.

Bad error handling is pervasive. The fact that everyone shows you code

that just logs the error is a prime example of this. It should be
considered a symptom of a problem, not an acceptable status quo to be
maintained. We need all the tools at our disposal to encourage better
thinking about and handling of errors. Most importantly, I think we need a
middle ground between completely untyped errors and an exhaustive list of
every possible error that might happen. I believe a well designed
mechanism for categorizing errors in a compiler-verified way can do exactly
this.

In many respects, there are similarities to this in the design of
`NSError` which provides categorization via the error domain. This
categorization is a bit more broad than I think is useful in many cases,
but it is the best example I'm aware of.

The primary difference between error domains and the kind of
categorization I am proposing is that error domains categorize based on the
source of an error whereas I am proposing categorization driven by likely
recovery strategies. Recovery is obviously application dependent, but I
think the example above demonstrates that there are some useful
generalizations that can be made (especially in an app-specific library),
even if they don't apply everywhere.

> Sometimes we then go on to have a conversation about wrapping errors in
other error types, and that can be interesting, but now we're talking about
adding a big, messy feature just to get "safety" guarantees for a fairly
minor need.

I think you're right that wrapping errors is tightly related to an
effective use of typed errors. You can do a reasonable job without
language support (as has been discussed on the list in the past). On the
other hand, if we're going to introduce typed errors we should do it in a
way that *encourages* effective use of them. My opinion is that
encouraging effect use means categorizing (wrapping) errors without
requiring any additional syntax beyond the simple `try` used by untyped
errors. In practice, this means we should not need to catch and rethrow an
error if all we want to do is categorize it. Rust provides good prior art
in this area.

>
> Programmers often have an instinct to obsess over error taxonomies that
is very rarely directed at solving any real problem; it is just
self-imposed busy-work.

I agree that obsessing over intricate taxonomies is counter-productive
and should be discouraged. On the other hand, I hope the example I
provided above can help to focus the discussion on a practical use of types
to categorize errors in a way that helps guide *thinking* and therefore
improves error handling in practice.

>
>> - Some people think that while it might be useful in some narrow
cases, the utility isn’t high enough to justify making the language more
complex (complexity that would intrude on the APIs of result types,
futures, etc)
>>
>> I’m sure there are other points in the discussion that I’m forgetting.
>>
>> One thing that I’m personally very concerned about is in the systems
programming domain. Systems code is sort of the classic example of code
that is low-level enough and finely specified enough that there are lots of
knowable things, including the failure modes.
>
> Here we are using "systems" to mean "embedded systems and kernels".
And frankly even a kernel is a large enough system that they don't want to
exhaustively switch over failures; they just want the static guarantees
that go along with a constrained error type.
>
>> Beyond expressivity though, our current model involves boxing thrown
values into an Error existential, something that forces an implicit memory
allocation when the value is large. Unless this is fixed, I’m very
concerned that we’ll end up with a situation where certain kinds of systems
code (i.e., that which cares about real time guarantees) will not be able
to use error handling at all.
>>
>> JohnMC has some ideas on how to change code generation for ‘throws’ to
avoid this problem, but I don’t understand his ideas enough to know if they
are practical and likely to happen or not.
>
> Essentially, you give Error a tagged-pointer representation to allow
payload-less errors on non-generic error types to be allocated globally,
and then you can (1) tell people to not throw errors that require
allocation if it's vital to avoid allocation (just like we would tell them
today not to construct classes or indirect enum cases) and (2) allow a
special global payload-less error to be substituted if error allocation
fails.
>
> Of course, we could also say that systems code is required to use a
typed-throws feature that we add down the line for their purposes. Or just
tell them to not use payloads. Or force them to constrain their error
types to fit within some given size. (Note that obsessive error taxonomies
tend to end up with a bunch of indirect enum cases anyway, because they get
recursive, so the allocation problem is very real whatever we do.)
>
> John.

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

Even for non-public code. The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch. People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws. Every. Single. Time.

We're doing it in the project I'm working on. Sure, there are some places where we just log the error. But the vast majority of the time, we're handling them. Maybe that's because we're using reactive programming and errors bubble to the top, so there's no need to write that many error handlers. And if I am just logging, either the error doesn't really matter or there is a TODO label reminding me to fix it at some point.

The primary goal for me personally is to factor out and centralize code that categorizes an error, allowing catch sites to focus on implementing recovery instead of figuring out what went wrong. Here’s some concrete sample code building on the example scenario above:

I'm using a similar approach. Here is some stripped down code:

//error object used throughout project
public struct Rc2Error: LocalizedError, CustomStringConvertible, CustomDebugStringConvertible {
  /// basic categories of errors
  public enum Rc2ErrorType: String, Error {
    /// a requested object was not found
    case noSuchElement
    /// a requested operation is already in progress
    case alreadyInProgress
    /// problem parsing json, Freddy error is nested
    case invalidJson
    /// nestedError will be the NSError
    case cocoa
    /// nested error is related to the file system
    case file
    /// a wrapped error from a websocket
    case websocket
    /// a generic network error
    case network
    /// an invalid argument was passed (or parsed from json)
    /// wraps an unknown error
    case unknown
  }

  /// the generic type of the error
  public let type: Rc2ErrorType
  /// the underlying error that caused the problem
  public let nestedError: Error?
  /// location in source code of where error happened
  public let location: String
}

//a domain-specific error type that will be nested
public enum NetworkingError {
  case unauthorized
  case unsupportedFileType
  case timeout
  case connectionError(Error)
  case canceled
  case uploadFailed(Error)
  case invalidHttpStatusCode(HTTPURLResponse)
  case restError(code: Int, message: String)
}

The most common errors don't need a nested error. The call site can figure out how to recover based on this. Using Result<T,E> I can specifically limit what kind of errors are possible from a function without using the wrapper error. E can always be specified as Error to ignore the typed system.

It would be great if the native swift error system let you optionally put compiler-enforced constraints on what kind of error can be thrown. Then I can setup handlers for my specific type of error, but the compiler will give an error/warning if I'm not handling a possible Error. A generic catch-all is not proper error handling. And if I'm calling something that doesn't throw type-constrained errors, I can use a generic handler and wrap it up in my own error. But the call site is getting details necessary to recover (if possible) without having to use the runtime to figure out what kind of error it is.

I've got a very efficient system set up right now with great error handling. I don't see why the same capability can't exist in the language, especially when you can choose to completely ignore it. Hopefully more people would use it and we'd stop seeing so many "unknown error" dialogs.

- Mark

···

On Aug 18, 2017, at 2:27 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:
On Aug 18, 2017, at 3:11 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 25, 2017, at 12:18 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote: on Fri Aug 18 2017, Joe Groff <swift-evolution@swift.org> wrote:

Essentially, you give Error a tagged-pointer representation to allow payload-less errors on non-generic error types to be allocated

globally, and then you can (1) tell people to not throw errors that require allocation if it's vital to avoid allocation (just like we would tell them today not to construct classes or indirect enum cases) and (2) allow a special global payload-less error to be substituted if error allocation fails. Of course, we could also say that systems code is required to use a typed-throws feature that we add down the line for their purposes. Or just tell them to not use payloads. Or force them to constrain their error types to fit within some given size. (Note that obsessive error taxonomies tend to end up with a bunch of indirect enum cases anyway, because they get recursive, so the allocation problem is very real whatever we do.)

Alternatively, with some LLVM work, we could have the thrower leave the error value on the stack when propagating an error, and make it the catcher's responsibility to consume the error value and pop the stack. We could make not only errors but existential returns in general more efficient and "systems code"-worthy with a technique like that.

That's how the best C++ unwiding mechanisms work already.

Them's fighting words. :)

The correct term, John, is “fightin'”

-Crustee

···

on Fri Aug 25 2017, John McCall <rjmccall-AT-apple.com> wrote:

On Aug 17, 2017, at 11:27 PM, John McCall via swift-evolution >>>> <swift-evolution@swift.org> wrote:

John.

I always thought it was off the table because we were wedded to the idea that throwing functions are just effectively returning a Result<T> normally under the covers, but if not, so much the better! -- -Dave _______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution

--
-Dave

Sent from my iPad

>> Splitting this off into its own thread:
>>
>>> One related topic that isn’t discussed is type errors. Many third party libraries use a Result type with typed errors. Moving to an async / await model without also introducing typed errors into Swift would require giving up something that is highly valued by many Swift developers. Maybe Swift 5 is the right time to tackle typed errors as well. I would be happy to help with design and drafting a proposal but would need collaborators on the implementation side.
>>
>> Typed throws is something we need to settle one way or the other, and I agree it would be nice to do that in the Swift 5 cycle.
>>
>> For the purposes of this sub-discussion, I think there are three kinds of code to think about:
>> 1) large scale API like Cocoa which evolve (adding significant functionality) over the course of many years and can’t break clients.
>> 2) the public API of shared swiftpm packages, whose lifecycle may rise and fall - being obsoleted and replaced by better packages if they encounter a design problem.
>> 3) internal APIs and applications, which are easy to change because the implementations and clients of the APIs are owned by the same people.
>>
>> These each have different sorts of concerns, and we hope that something can start out as #3 but work its way up the stack gracefully.
>>
>> Here is where I think things stand on it:
>> - There is consensus that untyped throws is the right thing for a large scale API like Cocoa. NSError is effectively proven here. Even if typed throws is introduced, Apple is unlikely to adopt it in their APIs for this reason.
>> - There is consensus that untyped throws is the right default for people to reach for for public package (#2).
>> - There is consensus that Java and other systems that encourage lists of throws error types lead to problematic APIs for a variety of reasons.
>> - There is disagreement about whether internal APIs (#3) should use it. It seems perfect to be able to write exhaustive catches in this situation, since everything in knowable. OTOH, this could encourage abuse of error handling in cases where you really should return an enum instead of using throws.
>> - Some people are concerned that introducing typed throws would cause people to reach for it instead of using untyped throws for public package APIs.
>
> Even for non-public code. The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch. People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws. Every. Single. Time.

I agree that exhaustive switching over errors is something that people are extremely likely to actually want to do. I also think it's a bit of a red herring. The value of typed errors is *not* in exhaustive switching. It is in categorization and verified documentation.

Here is a concrete example that applies to almost every app. When you make a network request there are many things that could go wrong to which you may want to respond differently:
* There might be no network available. You might recover by updating the UI to indicate that and start monitoring for a reachability change.
* There might have been a server error that should eventually be resolved (500). You might update the UI and provide the user the ability to retry.
* There might have been an unrecoverable server error (404). You will update the UI.
* There might have been a low level parsing error (bad JSON, etc). Recovery is perhaps similar in nature to #2, but the problem is less likely to be resolved quickly so you may not provide a retry option. You might also want to do something to notify your dev team that the server is returning JSON that can't be parsed.
* There might have been a higher-level parsing error (converting JSON to model types). This might be treated the same as bad JSON. On the other hand, depending on the specifics of the app, you might take an alternate path that only parses the most essential model data in hopes that the problem was somewhere else and this parse will succeed.

All of this can obviously be accomplished with untyped errors. That said, using types to categorize errors would significantly improve the clarity of such code. More importantly, I believe that by categorizing errors in ways that are most relevant to a specific domain a library (perhaps internal to an app) can encourage developers to think carefully about how to respond.

I used to be rather in favor of adding typed errors, thinking that it can only benefit and seemed reasonable. However, given the very interesting discussion here, I'm inclined to think that what you articulate above is actually a very good argument _against_ adding typed errors.

If I may simplify, the gist of the argument advanced by Tino, Charlie, and you is that the primary goal is documentation, and that documentation in the form of prose is insufficient because it can be unreliable. Therefore, you want a way for the compiler to enforce said documentation. (The categorization use case, I think, is well addressed by the protocol-based design discussed already in this thread.)

Actually documentation is only one of the goals I have and it is the least important. Please see my subsequent reply to John where I articulate the four primary goals I have for improved error handling, whether it be typed errors or some other mechanism. I am curious to see what you think of the goals, as well as what mechanism might best address those goals.

Your other three goals have to do with what you term categorization, unless I misunderstand. Are those not adequately addressed by Joe Groff's protocol-based design?

Can you elaborate on what you mean by Joe Gross’s protocol-based design? I certainly haven’t seen anything that I believe addresses those goals well.

···

On Aug 18, 2017, at 6:29 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Fri, Aug 18, 2017 at 6:19 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Aug 18, 2017, at 6:15 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:
On Fri, Aug 18, 2017 at 09:20 Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
On Aug 18, 2017, at 1:27 AM, John McCall <rjmccall@apple.com <mailto:rjmccall@apple.com>> wrote:
>> On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>>> On Aug 17, 2017, at 7:39 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

However, the compiler itself cannot reward, only punish in the form of errors or warnings; if exhaustive switching is a red herring and the payoff for typed errors is correct documentation, the effectiveness of this kind of compiler enforcement must be directly proportional to the degree of extrinsic punishment inflicted by the compiler (since the intrinsic reward of correct documentation is the same whether it's spelled using doc comments or the type system). This seems like a heavy-handed way to enforce documentation of only one specific aspect of a throwing function; moreover, if this use case were to be sufficiently compelling, then it's certainly a better argument for SourceKit (or some other builtin tool) to automatically generate information on all errors thrown than for the compiler to require that users declare it themselves--even if opt-in.

Bad error handling is pervasive. The fact that everyone shows you code that just logs the error is a prime example of this. It should be considered a symptom of a problem, not an acceptable status quo to be maintained. We need all the tools at our disposal to encourage better thinking about and handling of errors. Most importantly, I think we need a middle ground between completely untyped errors and an exhaustive list of every possible error that might happen. I believe a well designed mechanism for categorizing errors in a compiler-verified way can do exactly this.

In many respects, there are similarities to this in the design of `NSError` which provides categorization via the error domain. This categorization is a bit more broad than I think is useful in many cases, but it is the best example I'm aware of.

The primary difference between error domains and the kind of categorization I am proposing is that error domains categorize based on the source of an error whereas I am proposing categorization driven by likely recovery strategies. Recovery is obviously application dependent, but I think the example above demonstrates that there are some useful generalizations that can be made (especially in an app-specific library), even if they don't apply everywhere.

> Sometimes we then go on to have a conversation about wrapping errors in other error types, and that can be interesting, but now we're talking about adding a big, messy feature just to get "safety" guarantees for a fairly minor need.

I think you're right that wrapping errors is tightly related to an effective use of typed errors. You can do a reasonable job without language support (as has been discussed on the list in the past). On the other hand, if we're going to introduce typed errors we should do it in a way that *encourages* effective use of them. My opinion is that encouraging effect use means categorizing (wrapping) errors without requiring any additional syntax beyond the simple `try` used by untyped errors. In practice, this means we should not need to catch and rethrow an error if all we want to do is categorize it. Rust provides good prior art in this area.

>
> Programmers often have an instinct to obsess over error taxonomies that is very rarely directed at solving any real problem; it is just self-imposed busy-work.

I agree that obsessing over intricate taxonomies is counter-productive and should be discouraged. On the other hand, I hope the example I provided above can help to focus the discussion on a practical use of types to categorize errors in a way that helps guide *thinking* and therefore improves error handling in practice.

>
>> - Some people think that while it might be useful in some narrow cases, the utility isn’t high enough to justify making the language more complex (complexity that would intrude on the APIs of result types, futures, etc)
>>
>> I’m sure there are other points in the discussion that I’m forgetting.
>>
>> One thing that I’m personally very concerned about is in the systems programming domain. Systems code is sort of the classic example of code that is low-level enough and finely specified enough that there are lots of knowable things, including the failure modes.
>
> Here we are using "systems" to mean "embedded systems and kernels". And frankly even a kernel is a large enough system that they don't want to exhaustively switch over failures; they just want the static guarantees that go along with a constrained error type.
>
>> Beyond expressivity though, our current model involves boxing thrown values into an Error existential, something that forces an implicit memory allocation when the value is large. Unless this is fixed, I’m very concerned that we’ll end up with a situation where certain kinds of systems code (i.e., that which cares about real time guarantees) will not be able to use error handling at all.
>>
>> JohnMC has some ideas on how to change code generation for ‘throws’ to avoid this problem, but I don’t understand his ideas enough to know if they are practical and likely to happen or not.
>
> Essentially, you give Error a tagged-pointer representation to allow payload-less errors on non-generic error types to be allocated globally, and then you can (1) tell people to not throw errors that require allocation if it's vital to avoid allocation (just like we would tell them today not to construct classes or indirect enum cases) and (2) allow a special global payload-less error to be substituted if error allocation fails.
>
> Of course, we could also say that systems code is required to use a typed-throws feature that we add down the line for their purposes. Or just tell them to not use payloads. Or force them to constrain their error types to fit within some given size. (Note that obsessive error taxonomies tend to end up with a bunch of indirect enum cases anyway, because they get recursive, so the allocation problem is very real whatever we do.)
>
> John.

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

Even for non-public code. The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch. People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws. Every. Single. Time.

We're doing it in the project I'm working on. Sure, there are some places where we just log the error. But the vast majority of the time, we're handling them. Maybe that's because we're using reactive programming and errors bubble to the top, so there's no need to write that many error handlers. And if I am just logging, either the error doesn't really matter or there is a TODO label reminding me to fix it at some point.

I'm not saying people only log errors instead of handling them in some more reasonable way. I'm saying that logging functions are the only place I've ever seen someone switch over an entire error type.

I keep bringing exhaustive switches up because, as soon as you have a default case, it seems to me that you haven't really lost anything vs. starting from an opaque type like Error.

The primary goal for me personally is to factor out and centralize code that categorizes an error, allowing catch sites to focus on implementing recovery instead of figuring out what went wrong. Here’s some concrete sample code building on the example scenario above:

I'm using a similar approach. Here is some stripped down code:

//error object used throughout project
public struct Rc2Error: LocalizedError, CustomStringConvertible, CustomDebugStringConvertible {
  /// basic categories of errors
  public enum Rc2ErrorType: String, Error {
    /// a requested object was not found
    case noSuchElement
    /// a requested operation is already in progress
    case alreadyInProgress
    /// problem parsing json, Freddy error is nested
    case invalidJson
    /// nestedError will be the NSError
    case cocoa
    /// nested error is related to the file system
    case file
    /// a wrapped error from a websocket
    case websocket
    /// a generic network error
    case network
    /// an invalid argument was passed (or parsed from json)
    /// wraps an unknown error
    case unknown
  }

  /// the generic type of the error
  public let type: Rc2ErrorType
  /// the underlying error that caused the problem
  public let nestedError: Error?
  /// location in source code of where error happened
  public let location: String
}

Okay. I mean, this seems essentially like Swift's Error design. The comment says you use this type ubiquitously in your project. The type completely erases any differences between functions in terms of what errors they can throw. Predictably, it includes an unknown case. Code that processes values of this type must look essentially exactly like switching over an Error, except that the unknown case involves explicitly matching '.unknown' instead of using 'default'.

//a domain-specific error type that will be nested
public enum NetworkingError {
  case unauthorized
  case unsupportedFileType
  case timeout
  case connectionError(Error)
  case canceled
  case uploadFailed(Error)
  case invalidHttpStatusCode(HTTPURLResponse)
  case restError(code: Int, message: String)
}

Okay. So you're doing semantic tagging of errors — you're communicating out that an error arose during a specific operation. And having some static enforcement here makes it easier to ensure that you've tagged all the errors.

The most common errors don't need a nested error. The call site can figure out how to recover based on this. Using Result<T,E> I can specifically limit what kind of errors are possible from a function without using the wrapper error. E can always be specified as Error to ignore the typed system.

I see. So you do have some functions that use a more specific type.

It would be great if the native swift error system let you optionally put compiler-enforced constraints on what kind of error can be thrown. Then I can setup handlers for my specific type of error, but the compiler will give an error/warning if I'm not handling a possible Error. A generic catch-all is not proper error handling.

Do you really check for eight different cases at the call sites that get a NetworkingError, or is there a common predicate that you use? Is there a reason that predicate cannot be written on Error?

And if I'm calling something that doesn't throw type-constrained errors, I can use a generic handler and wrap it up in my own error. But the call site is getting details necessary to recover (if possible) without having to use the runtime to figure out what kind of error it is.

Can you talk about about why "without using the runtime" is an important requirement to you? Why is an enum-switch with a catch-all case acceptable but a type-switch with a default unacceptable?

I've got a very efficient system set up right now with great error handling. I don't see why the same capability can't exist in the language, especially when you can choose to completely ignore it. Hopefully more people would use it and we'd stop seeing so many "unknown error" dialogs.

John.

···

On Aug 18, 2017, at 11:43 PM, Mark Lilback <mark@lilback.com> wrote:

On Aug 18, 2017, at 2:27 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:
On Aug 18, 2017, at 3:11 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

Joe Groff wrote:

An alternative approach that embraces the open nature of errors could be to
represent domains as independent protocols, and extend the error types that
are relevant to that domain to conform to the protocol. That way, you don't
obscure the structure of the underlying error value with wrappers. If you
expect to exhaustively handle all errors in a domain, well, you'd almost
certainly going to need to have a fallback case in your wrapper type for
miscellaneous errors, but you could represent that instead without wrapping
via a catch-all, and as?-casting to your domain protocol with a ??-default
for errors that don't conform to the protocol. For example, instead of
attempting something like this:

enum DatabaseError {
  case queryError(QueryError)
  case ioError(IOError)
  case other(Error)

  var errorKind: String {
    switch self {
      case .queryError(let q): return "query error: \(q.query)"
      case .ioError(let i): return "io error: \(i.filename)"
      case .other(let e): return "\(e)"
    }
  }
}

func queryDatabase(_ query: String) throws /*DatabaseError*/ -> Table

do {
  queryDatabase("delete * from users")
} catch let d as DatabaseError {
  os_log(d.errorKind)
} catch {
  fatalError("unexpected non-database error")
}

You could do this:

protocol DatabaseError {
  var errorKind: String { get }
}

extension QueryError: DatabaseError {
  var errorKind: String { return "query error: \(q.query)" }
}
extension IOError: DatabaseError {
  var errorKind: String ( return "io error: \(i.filename)" }
}

extension Error {
  var databaseErrorKind: String {
    return (error as? DatabaseError)?.errorKind ?? "unexpected non-database
error"
  }
}

func queryDatabase(_ query: String) throws -> Table

do {
  queryDatabase("delete * from users")
} catch {
  os_log(error.databaseErrorKind)
}

···

On Fri, Aug 18, 2017 at 6:46 PM, Matthew Johnson <matthew@anandabits.com> wrote:

On Aug 18, 2017, at 6:29 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Fri, Aug 18, 2017 at 6:19 PM, Matthew Johnson <matthew@anandabits.com> > wrote:

On Aug 18, 2017, at 6:15 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Fri, Aug 18, 2017 at 09:20 Matthew Johnson via swift-evolution < >> swift-evolution@swift.org> wrote:

Sent from my iPad

On Aug 18, 2017, at 1:27 AM, John McCall <rjmccall@apple.com> wrote:

>> On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution < >>> swift-evolution@swift.org> wrote:
>> Splitting this off into its own thread:
>>
>>> On Aug 17, 2017, at 7:39 PM, Matthew Johnson <matthew@anandabits.com> >>> wrote:
>>> One related topic that isn’t discussed is type errors. Many third
party libraries use a Result type with typed errors. Moving to an async /
await model without also introducing typed errors into Swift would require
giving up something that is highly valued by many Swift developers. Maybe
Swift 5 is the right time to tackle typed errors as well. I would be happy
to help with design and drafting a proposal but would need collaborators on
the implementation side.
>>
>> Typed throws is something we need to settle one way or the other, and
I agree it would be nice to do that in the Swift 5 cycle.
>>
>> For the purposes of this sub-discussion, I think there are three
kinds of code to think about:
>> 1) large scale API like Cocoa which evolve (adding significant
functionality) over the course of many years and can’t break clients.
>> 2) the public API of shared swiftpm packages, whose lifecycle may
rise and fall - being obsoleted and replaced by better packages if they
encounter a design problem.
>> 3) internal APIs and applications, which are easy to change because
the implementations and clients of the APIs are owned by the same people.
>>
>> These each have different sorts of concerns, and we hope that
something can start out as #3 but work its way up the stack gracefully.
>>
>> Here is where I think things stand on it:
>> - There is consensus that untyped throws is the right thing for a
large scale API like Cocoa. NSError is effectively proven here. Even if
typed throws is introduced, Apple is unlikely to adopt it in their APIs for
this reason.
>> - There is consensus that untyped throws is the right default for
people to reach for for public package (#2).
>> - There is consensus that Java and other systems that encourage lists
of throws error types lead to problematic APIs for a variety of reasons.
>> - There is disagreement about whether internal APIs (#3) should use
it. It seems perfect to be able to write exhaustive catches in this
situation, since everything in knowable. OTOH, this could encourage abuse
of error handling in cases where you really should return an enum instead
of using throws.
>> - Some people are concerned that introducing typed throws would cause
people to reach for it instead of using untyped throws for public package
APIs.
>
> Even for non-public code. The only practical merit of typed throws I
have ever seen someone demonstrate is that it would let them use contextual
lookup in a throw or catch. People always say "I'll be able to
exhaustively switch over my errors", and then I ask them to show me where
they want to do that, and they show me something that just logs the error,
which of course does not require typed throws. Every. Single. Time.

I agree that exhaustive switching over errors is something that people
are extremely likely to actually want to do. I also think it's a bit of a
red herring. The value of typed errors is *not* in exhaustive switching.
It is in categorization and verified documentation.

Here is a concrete example that applies to almost every app. When you
make a network request there are many things that could go wrong to which
you may want to respond differently:
* There might be no network available. You might recover by updating
the UI to indicate that and start monitoring for a reachability change.
* There might have been a server error that should eventually be
resolved (500). You might update the UI and provide the user the ability
to retry.
* There might have been an unrecoverable server error (404). You will
update the UI.
* There might have been a low level parsing error (bad JSON, etc).
Recovery is perhaps similar in nature to #2, but the problem is less likely
to be resolved quickly so you may not provide a retry option. You might
also want to do something to notify your dev team that the server is
returning JSON that can't be parsed.
* There might have been a higher-level parsing error (converting JSON to
model types). This might be treated the same as bad JSON. On the other
hand, depending on the specifics of the app, you might take an alternate
path that only parses the most essential model data in hopes that the
problem was somewhere else and this parse will succeed.

All of this can obviously be accomplished with untyped errors. That
said, using types to categorize errors would significantly improve the
clarity of such code. More importantly, I believe that by categorizing
errors in ways that are most relevant to a specific domain a library
(perhaps internal to an app) can encourage developers to think carefully
about how to respond.

I used to be rather in favor of adding typed errors, thinking that it can
only benefit and seemed reasonable. However, given the very interesting
discussion here, I'm inclined to think that what you articulate above is
actually a very good argument _against_ adding typed errors.

If I may simplify, the gist of the argument advanced by Tino, Charlie,
and you is that the primary goal is documentation, and that documentation
in the form of prose is insufficient because it can be unreliable.
Therefore, you want a way for the compiler to enforce said documentation.
(The categorization use case, I think, is well addressed by the
protocol-based design discussed already in this thread.)

Actually documentation is only one of the goals I have and it is the
least important. Please see my subsequent reply to John where I articulate
the four primary goals I have for improved error handling, whether it be
typed errors or some other mechanism. I am curious to see what you think
of the goals, as well as what mechanism might best address those goals.

Your other three goals have to do with what you term categorization,
unless I misunderstand. Are those not adequately addressed by Joe Groff's
protocol-based design?

Can you elaborate on what you mean by Joe Gross’s protocol-based design?
I certainly haven’t seen anything that I believe addresses those goals well.

However, the compiler itself cannot reward, only punish in the form of
errors or warnings; if exhaustive switching is a red herring and the payoff
for typed errors is correct documentation, the effectiveness of this kind
of compiler enforcement must be directly proportional to the degree of
extrinsic punishment inflicted by the compiler (since the intrinsic reward
of correct documentation is the same whether it's spelled using doc
comments or the type system). This seems like a heavy-handed way to enforce
documentation of only one specific aspect of a throwing function; moreover,
if this use case were to be sufficiently compelling, then it's certainly a
better argument for SourceKit (or some other builtin tool) to automatically
generate information on all errors thrown than for the compiler to require
that users declare it themselves--even if opt-in.

Bad error handling is pervasive. The fact that everyone shows you code

that just logs the error is a prime example of this. It should be
considered a symptom of a problem, not an acceptable status quo to be
maintained. We need all the tools at our disposal to encourage better
thinking about and handling of errors. Most importantly, I think we need a
middle ground between completely untyped errors and an exhaustive list of
every possible error that might happen. I believe a well designed
mechanism for categorizing errors in a compiler-verified way can do exactly
this.

In many respects, there are similarities to this in the design of
`NSError` which provides categorization via the error domain. This
categorization is a bit more broad than I think is useful in many cases,
but it is the best example I'm aware of.

The primary difference between error domains and the kind of
categorization I am proposing is that error domains categorize based on the
source of an error whereas I am proposing categorization driven by likely
recovery strategies. Recovery is obviously application dependent, but I
think the example above demonstrates that there are some useful
generalizations that can be made (especially in an app-specific library),
even if they don't apply everywhere.

> Sometimes we then go on to have a conversation about wrapping errors
in other error types, and that can be interesting, but now we're talking
about adding a big, messy feature just to get "safety" guarantees for a
fairly minor need.

I think you're right that wrapping errors is tightly related to an
effective use of typed errors. You can do a reasonable job without
language support (as has been discussed on the list in the past). On the
other hand, if we're going to introduce typed errors we should do it in a
way that *encourages* effective use of them. My opinion is that
encouraging effect use means categorizing (wrapping) errors without
requiring any additional syntax beyond the simple `try` used by untyped
errors. In practice, this means we should not need to catch and rethrow an
error if all we want to do is categorize it. Rust provides good prior art
in this area.

>
> Programmers often have an instinct to obsess over error taxonomies
that is very rarely directed at solving any real problem; it is just
self-imposed busy-work.

I agree that obsessing over intricate taxonomies is counter-productive
and should be discouraged. On the other hand, I hope the example I
provided above can help to focus the discussion on a practical use of types
to categorize errors in a way that helps guide *thinking* and therefore
improves error handling in practice.

>
>> - Some people think that while it might be useful in some narrow
cases, the utility isn’t high enough to justify making the language more
complex (complexity that would intrude on the APIs of result types,
futures, etc)
>>
>> I’m sure there are other points in the discussion that I’m forgetting.
>>
>> One thing that I’m personally very concerned about is in the systems
programming domain. Systems code is sort of the classic example of code
that is low-level enough and finely specified enough that there are lots of
knowable things, including the failure modes.
>
> Here we are using "systems" to mean "embedded systems and kernels".
And frankly even a kernel is a large enough system that they don't want to
exhaustively switch over failures; they just want the static guarantees
that go along with a constrained error type.
>
>> Beyond expressivity though, our current model involves boxing thrown
values into an Error existential, something that forces an implicit memory
allocation when the value is large. Unless this is fixed, I’m very
concerned that we’ll end up with a situation where certain kinds of systems
code (i.e., that which cares about real time guarantees) will not be able
to use error handling at all.
>>
>> JohnMC has some ideas on how to change code generation for ‘throws’
to avoid this problem, but I don’t understand his ideas enough to know if
they are practical and likely to happen or not.
>
> Essentially, you give Error a tagged-pointer representation to allow
payload-less errors on non-generic error types to be allocated globally,
and then you can (1) tell people to not throw errors that require
allocation if it's vital to avoid allocation (just like we would tell them
today not to construct classes or indirect enum cases) and (2) allow a
special global payload-less error to be substituted if error allocation
fails.
>
> Of course, we could also say that systems code is required to use a
typed-throws feature that we add down the line for their purposes. Or just
tell them to not use payloads. Or force them to constrain their error
types to fit within some given size. (Note that obsessive error taxonomies
tend to end up with a bunch of indirect enum cases anyway, because they get
recursive, so the allocation problem is very real whatever we do.)
>
> John.

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

Even for non-public code. The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch. People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws. Every. Single. Time.

We're doing it in the project I'm working on. Sure, there are some places where we just log the error. But the vast majority of the time, we're handling them. Maybe that's because we're using reactive programming and errors bubble to the top, so there's no need to write that many error handlers. And if I am just logging, either the error doesn't really matter or there is a TODO label reminding me to fix it at some point.

I'm not saying people only log errors instead of handling them in some more reasonable way. I'm saying that logging functions are the only place I've ever seen someone switch over an entire error type.

I keep bringing exhaustive switches up because, as soon as you have a default case, it seems to me that you haven't really lost anything vs. starting from an opaque type like Error.

The primary goal for me personally is to factor out and centralize code that categorizes an error, allowing catch sites to focus on implementing recovery instead of figuring out what went wrong. Here’s some concrete sample code building on the example scenario above:

I'm using a similar approach. Here is some stripped down code:

//error object used throughout project
public struct Rc2Error: LocalizedError, CustomStringConvertible, CustomDebugStringConvertible {
  /// basic categories of errors
  public enum Rc2ErrorType: String, Error {
    /// a requested object was not found
    case noSuchElement
    /// a requested operation is already in progress
    case alreadyInProgress
    /// problem parsing json, Freddy error is nested
    case invalidJson
    /// nestedError will be the NSError
    case cocoa
    /// nested error is related to the file system
    case file
    /// a wrapped error from a websocket
    case websocket
    /// a generic network error
    case network
    /// an invalid argument was passed (or parsed from json)
    /// wraps an unknown error
    case unknown
  }

  /// the generic type of the error
  public let type: Rc2ErrorType
  /// the underlying error that caused the problem
  public let nestedError: Error?
  /// location in source code of where error happened
  public let location: String
}

Okay. I mean, this seems essentially like Swift's Error design. The comment says you use this type ubiquitously in your project. The type completely erases any differences between functions in terms of what errors they can throw. Predictably, it includes an unknown case. Code that processes values of this type must look essentially exactly like switching over an Error, except that the unknown case involves explicitly matching '.unknown' instead of using 'default'.

//a domain-specific error type that will be nested
public enum NetworkingError {
  case unauthorized
  case unsupportedFileType
  case timeout
  case connectionError(Error)
  case canceled
  case uploadFailed(Error)
  case invalidHttpStatusCode(HTTPURLResponse)
  case restError(code: Int, message: String)
}

Okay. So you're doing semantic tagging of errors — you're communicating out that an error arose during a specific operation. And having some static enforcement here makes it easier to ensure that you've tagged all the errors.

The most common errors don't need a nested error. The call site can figure out how to recover based on this. Using Result<T,E> I can specifically limit what kind of errors are possible from a function without using the wrapper error. E can always be specified as Error to ignore the typed system.

I see. So you do have some functions that use a more specific type.

It would be great if the native swift error system let you optionally put compiler-enforced constraints on what kind of error can be thrown. Then I can setup handlers for my specific type of error, but the compiler will give an error/warning if I'm not handling a possible Error. A generic catch-all is not proper error handling.

Do you really check for eight different cases at the call sites that get a NetworkingError, or is there a common predicate that you use? Is there a reason that predicate cannot be written on Error?

And if I'm calling something that doesn't throw type-constrained errors, I can use a generic handler and wrap it up in my own error. But the call site is getting details necessary to recover (if possible) without having to use the runtime to figure out what kind of error it is.

Can you talk about about why "without using the runtime" is an important requirement to you? Why is an enum-switch with a catch-all case acceptable but a type-switch with a default unacceptable?

I've realized that these questions might seem rhetorical or leading, and I'd like to clarify that they're not meant that way. I am earnestly trying to explore your use pattern here and the ways in which our current design falls short of what you want to do. In order to do that, I have to understand why you're approaching things a certain way.

What you've pointed out about being able to statically detect failures to remap errors into your operational-tagging schema seems quite credible to me. Some of these other things do ring a little more hollow, in the sense that there doesn't seem to be anything stopping you from applying them to Error if you can apply them to Rc2Error. However, it's entirely possible that I'm missing something, and I'm trying to understand.

John.

···

On Aug 19, 2017, at 12:25 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 18, 2017, at 11:43 PM, Mark Lilback <mark@lilback.com <mailto:mark@lilback.com>> wrote:

On Aug 18, 2017, at 2:27 AM, John McCall via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
On Aug 18, 2017, at 3:11 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

I've got a very efficient system set up right now with great error handling. I don't see why the same capability can't exist in the language, especially when you can choose to completely ignore it. Hopefully more people would use it and we'd stop seeing so many "unknown error" dialogs.

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

Even for non-public code. The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch. People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws. Every. Single. Time.

We're doing it in the project I'm working on. Sure, there are some places where we just log the error. But the vast majority of the time, we're handling them. Maybe that's because we're using reactive programming and errors bubble to the top, so there's no need to write that many error handlers. And if I am just logging, either the error doesn't really matter or there is a TODO label reminding me to fix it at some point.

I'm not saying people only log errors instead of handling them in some more reasonable way. I'm saying that logging functions are the only place I've ever seen someone switch over an entire error type.

I keep bringing exhaustive switches up because, as soon as you have a default case, it seems to me that you haven't really lost anything vs. starting from an opaque type like Error.

If the bar for typed errors is going to be exhaustive handling without an "other" / "unknown" then I doubt we will be able to meet it. That is possible and useful in some kinds of code but will certainly continue to be the exception.

On the other hand, if the bar is more generally whether typed errors can improve error handling in practice to a sufficient degree to justify the feature I think there is a good chance that they can, given the right design.

Currently good error handling often requires a lot of time reading documentation, and often a lot of time trying to *find* the right documentation. All too often that documentation doesn't even exist or is spotty and out of date. There has been more than one occasion where the only way to get the information I needed was to read the source code of a dependency (thankfully they were open source). A language is obviously not expected to solve the problem of poor documentation, but it can and should help surface more information regardless of the state of documentation.

Once you have the necessary information it is often necessary to write tedious error analysis code to categorize the error appropriately for the purpose of recovery.

I believe these problems are a significant driver behind the sad state of error handling in many (probably most) apps. If a library author believes they have sufficient information about usage to categorize errors in a way that will be useful in practice for the majority of their users the language should support the library author in doing this. Again, this is specifically *not* about providing an exhaustive list of every possible kind of error that might occur. It is about categorizing errors based on anticipated recovery strategy (while still retaining the underlying error information for cases where the catch site requires it).

Types seem like a convenient way to accomplish the goal of categorization. They are also already in use by at least some of us, but unfortunately the type information is currently discarded by the language. This is unfortunate for callers and can also lead to bugs where an error that should have been categorized leaks out because it wasn't wrapped as intended.

···

Sent from my iPad

On Aug 18, 2017, at 11:24 PM, John McCall <rjmccall@apple.com> wrote:

On Aug 18, 2017, at 11:43 PM, Mark Lilback <mark@lilback.com> wrote:
On Aug 18, 2017, at 2:27 AM, John McCall via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 18, 2017, at 3:11 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

The primary goal for me personally is to factor out and centralize code that categorizes an error, allowing catch sites to focus on implementing recovery instead of figuring out what went wrong. Here’s some concrete sample code building on the example scenario above:

I'm using a similar approach. Here is some stripped down code:

//error object used throughout project
public struct Rc2Error: LocalizedError, CustomStringConvertible, CustomDebugStringConvertible {
   /// basic categories of errors
   public enum Rc2ErrorType: String, Error {
       /// a requested object was not found
       case noSuchElement
       /// a requested operation is already in progress
       case alreadyInProgress
       /// problem parsing json, Freddy error is nested
       case invalidJson
       /// nestedError will be the NSError
       case cocoa
       /// nested error is related to the file system
       case file
       /// a wrapped error from a websocket
       case websocket
       /// a generic network error
       case network
       /// an invalid argument was passed (or parsed from json)
       /// wraps an unknown error
       case unknown
   }

   /// the generic type of the error
   public let type: Rc2ErrorType
   /// the underlying error that caused the problem
   public let nestedError: Error?
   /// location in source code of where error happened
   public let location: String
}

Okay. I mean, this seems essentially like Swift's Error design. The comment says you use this type ubiquitously in your project. The type completely erases any differences between functions in terms of what errors they can throw. Predictably, it includes an unknown case. Code that processes values of this type must look essentially exactly like switching over an Error, except that the unknown case involves explicitly matching '.unknown' instead of using 'default'.

//a domain-specific error type that will be nested
public enum NetworkingError {
   case unauthorized
   case unsupportedFileType
   case timeout
   case connectionError(Error)
   case canceled
   case uploadFailed(Error)
   case invalidHttpStatusCode(HTTPURLResponse)
   case restError(code: Int, message: String)
}

Okay. So you're doing semantic tagging of errors — you're communicating out that an error arose during a specific operation. And having some static enforcement here makes it easier to ensure that you've tagged all the errors.

The most common errors don't need a nested error. The call site can figure out how to recover based on this. Using Result<T,E> I can specifically limit what kind of errors are possible from a function without using the wrapper error. E can always be specified as Error to ignore the typed system.

I see. So you do have some functions that use a more specific type.

It would be great if the native swift error system let you optionally put compiler-enforced constraints on what kind of error can be thrown. Then I can setup handlers for my specific type of error, but the compiler will give an error/warning if I'm not handling a possible Error. A generic catch-all is not proper error handling.

Do you really check for eight different cases at the call sites that get a NetworkingError, or is there a common predicate that you use? Is there a reason that predicate cannot be written on Error?

And if I'm calling something that doesn't throw type-constrained errors, I can use a generic handler and wrap it up in my own error. But the call site is getting details necessary to recover (if possible) without having to use the runtime to figure out what kind of error it is.

Can you talk about about why "without using the runtime" is an important requirement to you? Why is an enum-switch with a catch-all case acceptable but a type-switch with a default unacceptable?

I've got a very efficient system set up right now with great error handling. I don't see why the same capability can't exist in the language, especially when you can choose to completely ignore it. Hopefully more people would use it and we'd stop seeing so many "unknown error" dialogs.

John.

Joe Groff wrote:

An alternative approach that embraces the open nature of errors could be to represent domains as independent protocols, and extend the error types that are relevant to that domain to conform to the protocol. That way, you don't obscure the structure of the underlying error value with wrappers. If you expect to exhaustively handle all errors in a domain, well, you'd almost certainly going to need to have a fallback case in your wrapper type for miscellaneous errors, but you could represent that instead without wrapping via a catch-all, and as?-casting to your domain protocol with a ??-default for errors that don't conform to the protocol. For example, instead of attempting something like this:

enum DatabaseError {
  case queryError(QueryError)
  case ioError(IOError)
  case other(Error)

  var errorKind: String {
    switch self {
      case .queryError(let q): return "query error: \(q.query)"
      case .ioError(let i): return "io error: \(i.filename)"
      case .other(let e): return "\(e)"
    }
  }
}

func queryDatabase(_ query: String) throws /*DatabaseError*/ -> Table

do {
  queryDatabase("delete * from users")
} catch let d as DatabaseError {
  os_log(d.errorKind)
} catch {
  fatalError("unexpected non-database error")
}

You could do this:

protocol DatabaseError {
  var errorKind: String { get }
}

extension QueryError: DatabaseError {
  var errorKind: String { return "query error: \(q.query)" }
}
extension IOError: DatabaseError {
  var errorKind: String ( return "io error: \(i.filename)" }
}

extension Error {
  var databaseErrorKind: String {
    return (error as? DatabaseError)?.errorKind ?? "unexpected non-database error"
  }
}

func queryDatabase(_ query: String) throws -> Table

do {
  queryDatabase("delete * from users")
} catch {
  os_log(error.databaseErrorKind)
}

This approach isn't sufficient for several reasons. Notably, it requires the underlying errors to already have a distinct type for every category we wish to place them in. If all network errors have the same type and I want to categorize them based on network availability, authentication, dropped connection, etc I am not able to do that.

The kind of categorization I want to be able to do requires a custom algorithm. The specific algorithm is used to categorize errors depends on the dynamic context (i.e. the function that is propagating it). The way I usually think about this categorization is as a conversion initializer as I showed in the example, but it certainly wouldn't need to be accomplished that way. The most important thing IMO is the ability to categorize during error propagation and make information about that categorization easy for callers to discover.

The output of the algorithm could use various mechanisms for categorization - an enum is one mechanism, distinct types conforming to appropriate categorization protocols is another. Attaching some kind of category value to the original error or propagating the category along with it might also work (although might be rather clunky).

It is trivial to make the original error immediately available via an `underlyingError` property so I really don't understand the resistance to wrapping errors. The categorization can easily be ignored at the catch site if desired. That said, if we figure out some other mechanism for categorizing errors, including placing different error values of the same type into different categories, and matching them based on this categorization I think I would be ok with that. Using wrapper types is not essential to solving the problem.

Setting all of this aside, surely you had you had your own reasons for supporting typed errors in the past. What were those and why do you no longer consider them important?

···

Sent from my iPad

On Aug 18, 2017, at 6:56 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Fri, Aug 18, 2017 at 6:46 PM, Matthew Johnson <matthew@anandabits.com> wrote:

On Aug 18, 2017, at 6:29 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Fri, Aug 18, 2017 at 6:19 PM, Matthew Johnson <matthew@anandabits.com> wrote:

On Aug 18, 2017, at 6:15 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Fri, Aug 18, 2017 at 09:20 Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

Sent from my iPad

On Aug 18, 2017, at 1:27 AM, John McCall <rjmccall@apple.com> wrote:

>> On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:
>> Splitting this off into its own thread:
>>
>>> On Aug 17, 2017, at 7:39 PM, Matthew Johnson <matthew@anandabits.com> wrote:
>>> One related topic that isn’t discussed is type errors. Many third party libraries use a Result type with typed errors. Moving to an async / await model without also introducing typed errors into Swift would require giving up something that is highly valued by many Swift developers. Maybe Swift 5 is the right time to tackle typed errors as well. I would be happy to help with design and drafting a proposal but would need collaborators on the implementation side.
>>
>> Typed throws is something we need to settle one way or the other, and I agree it would be nice to do that in the Swift 5 cycle.
>>
>> For the purposes of this sub-discussion, I think there are three kinds of code to think about:
>> 1) large scale API like Cocoa which evolve (adding significant functionality) over the course of many years and can’t break clients.
>> 2) the public API of shared swiftpm packages, whose lifecycle may rise and fall - being obsoleted and replaced by better packages if they encounter a design problem.
>> 3) internal APIs and applications, which are easy to change because the implementations and clients of the APIs are owned by the same people.
>>
>> These each have different sorts of concerns, and we hope that something can start out as #3 but work its way up the stack gracefully.
>>
>> Here is where I think things stand on it:
>> - There is consensus that untyped throws is the right thing for a large scale API like Cocoa. NSError is effectively proven here. Even if typed throws is introduced, Apple is unlikely to adopt it in their APIs for this reason.
>> - There is consensus that untyped throws is the right default for people to reach for for public package (#2).
>> - There is consensus that Java and other systems that encourage lists of throws error types lead to problematic APIs for a variety of reasons.
>> - There is disagreement about whether internal APIs (#3) should use it. It seems perfect to be able to write exhaustive catches in this situation, since everything in knowable. OTOH, this could encourage abuse of error handling in cases where you really should return an enum instead of using throws.
>> - Some people are concerned that introducing typed throws would cause people to reach for it instead of using untyped throws for public package APIs.
>
> Even for non-public code. The only practical merit of typed throws I have ever seen someone demonstrate is that it would let them use contextual lookup in a throw or catch. People always say "I'll be able to exhaustively switch over my errors", and then I ask them to show me where they want to do that, and they show me something that just logs the error, which of course does not require typed throws. Every. Single. Time.

I agree that exhaustive switching over errors is something that people are extremely likely to actually want to do. I also think it's a bit of a red herring. The value of typed errors is *not* in exhaustive switching. It is in categorization and verified documentation.

Here is a concrete example that applies to almost every app. When you make a network request there are many things that could go wrong to which you may want to respond differently:
* There might be no network available. You might recover by updating the UI to indicate that and start monitoring for a reachability change.
* There might have been a server error that should eventually be resolved (500). You might update the UI and provide the user the ability to retry.
* There might have been an unrecoverable server error (404). You will update the UI.
* There might have been a low level parsing error (bad JSON, etc). Recovery is perhaps similar in nature to #2, but the problem is less likely to be resolved quickly so you may not provide a retry option. You might also want to do something to notify your dev team that the server is returning JSON that can't be parsed.
* There might have been a higher-level parsing error (converting JSON to model types). This might be treated the same as bad JSON. On the other hand, depending on the specifics of the app, you might take an alternate path that only parses the most essential model data in hopes that the problem was somewhere else and this parse will succeed.

All of this can obviously be accomplished with untyped errors. That said, using types to categorize errors would significantly improve the clarity of such code. More importantly, I believe that by categorizing errors in ways that are most relevant to a specific domain a library (perhaps internal to an app) can encourage developers to think carefully about how to respond.

I used to be rather in favor of adding typed errors, thinking that it can only benefit and seemed reasonable. However, given the very interesting discussion here, I'm inclined to think that what you articulate above is actually a very good argument _against_ adding typed errors.

If I may simplify, the gist of the argument advanced by Tino, Charlie, and you is that the primary goal is documentation, and that documentation in the form of prose is insufficient because it can be unreliable. Therefore, you want a way for the compiler to enforce said documentation. (The categorization use case, I think, is well addressed by the protocol-based design discussed already in this thread.)

Actually documentation is only one of the goals I have and it is the least important. Please see my subsequent reply to John where I articulate the four primary goals I have for improved error handling, whether it be typed errors or some other mechanism. I am curious to see what you think of the goals, as well as what mechanism might best address those goals.

Your other three goals have to do with what you term categorization, unless I misunderstand. Are those not adequately addressed by Joe Groff's protocol-based design?

Can you elaborate on what you mean by Joe Gross’s protocol-based design? I certainly haven’t seen anything that I believe addresses those goals well.

However, the compiler itself cannot reward, only punish in the form of errors or warnings; if exhaustive switching is a red herring and the payoff for typed errors is correct documentation, the effectiveness of this kind of compiler enforcement must be directly proportional to the degree of extrinsic punishment inflicted by the compiler (since the intrinsic reward of correct documentation is the same whether it's spelled using doc comments or the type system). This seems like a heavy-handed way to enforce documentation of only one specific aspect of a throwing function; moreover, if this use case were to be sufficiently compelling, then it's certainly a better argument for SourceKit (or some other builtin tool) to automatically generate information on all errors thrown than for the compiler to require that users declare it themselves--even if opt-in.

Bad error handling is pervasive. The fact that everyone shows you code that just logs the error is a prime example of this. It should be considered a symptom of a problem, not an acceptable status quo to be maintained. We need all the tools at our disposal to encourage better thinking about and handling of errors. Most importantly, I think we need a middle ground between completely untyped errors and an exhaustive list of every possible error that might happen. I believe a well designed mechanism for categorizing errors in a compiler-verified way can do exactly this.

In many respects, there are similarities to this in the design of `NSError` which provides categorization via the error domain. This categorization is a bit more broad than I think is useful in many cases, but it is the best example I'm aware of.

The primary difference between error domains and the kind of categorization I am proposing is that error domains categorize based on the source of an error whereas I am proposing categorization driven by likely recovery strategies. Recovery is obviously application dependent, but I think the example above demonstrates that there are some useful generalizations that can be made (especially in an app-specific library), even if they don't apply everywhere.

> Sometimes we then go on to have a conversation about wrapping errors in other error types, and that can be interesting, but now we're talking about adding a big, messy feature just to get "safety" guarantees for a fairly minor need.

I think you're right that wrapping errors is tightly related to an effective use of typed errors. You can do a reasonable job without language support (as has been discussed on the list in the past). On the other hand, if we're going to introduce typed errors we should do it in a way that *encourages* effective use of them. My opinion is that encouraging effect use means categorizing (wrapping) errors without requiring any additional syntax beyond the simple `try` used by untyped errors. In practice, this means we should not need to catch and rethrow an error if all we want to do is categorize it. Rust provides good prior art in this area.

>
> Programmers often have an instinct to obsess over error taxonomies that is very rarely directed at solving any real problem; it is just self-imposed busy-work.

I agree that obsessing over intricate taxonomies is counter-productive and should be discouraged. On the other hand, I hope the example I provided above can help to focus the discussion on a practical use of types to categorize errors in a way that helps guide *thinking* and therefore improves error handling in practice.

>
>> - Some people think that while it might be useful in some narrow cases, the utility isn’t high enough to justify making the language more complex (complexity that would intrude on the APIs of result types, futures, etc)
>>
>> I’m sure there are other points in the discussion that I’m forgetting.
>>
>> One thing that I’m personally very concerned about is in the systems programming domain. Systems code is sort of the classic example of code that is low-level enough and finely specified enough that there are lots of knowable things, including the failure modes.
>
> Here we are using "systems" to mean "embedded systems and kernels". And frankly even a kernel is a large enough system that they don't want to exhaustively switch over failures; they just want the static guarantees that go along with a constrained error type.
>
>> Beyond expressivity though, our current model involves boxing thrown values into an Error existential, something that forces an implicit memory allocation when the value is large. Unless this is fixed, I’m very concerned that we’ll end up with a situation where certain kinds of systems code (i.e., that which cares about real time guarantees) will not be able to use error handling at all.
>>
>> JohnMC has some ideas on how to change code generation for ‘throws’ to avoid this problem, but I don’t understand his ideas enough to know if they are practical and likely to happen or not.
>
> Essentially, you give Error a tagged-pointer representation to allow payload-less errors on non-generic error types to be allocated globally, and then you can (1) tell people to not throw errors that require allocation if it's vital to avoid allocation (just like we would tell them today not to construct classes or indirect enum cases) and (2) allow a special global payload-less error to be substituted if error allocation fails.
>
> Of course, we could also say that systems code is required to use a typed-throws feature that we add down the line for their purposes. Or just tell them to not use payloads. Or force them to constrain their error types to fit within some given size. (Note that obsessive error taxonomies tend to end up with a bunch of indirect enum cases anyway, because they get recursive, so the allocation problem is very real whatever we do.)
>
> John.

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

Sent from my iPad

Joe Groff wrote:

An alternative approach that embraces the open nature of errors could be
to represent domains as independent protocols, and extend the error types
that are relevant to that domain to conform to the protocol. That way, you
don't obscure the structure of the underlying error value with wrappers. If
you expect to exhaustively handle all errors in a domain, well, you'd
almost certainly going to need to have a fallback case in your wrapper type
for miscellaneous errors, but you could represent that instead without
wrapping via a catch-all, and as?-casting to your domain protocol with a
??-default for errors that don't conform to the protocol. For example,
instead of attempting something like this:

enum DatabaseError {
  case queryError(QueryError)
  case ioError(IOError)
  case other(Error)

  var errorKind: String {
    switch self {
      case .queryError(let q): return "query error: \(q.query)"
      case .ioError(let i): return "io error: \(i.filename)"
      case .other(let e): return "\(e)"
    }
  }
}

func queryDatabase(_ query: String) throws /*DatabaseError*/ -> Table

do {
  queryDatabase("delete * from users")
} catch let d as DatabaseError {
  os_log(d.errorKind)
} catch {
  fatalError("unexpected non-database error")
}

You could do this:

protocol DatabaseError {
  var errorKind: String { get }
}

extension QueryError: DatabaseError {
  var errorKind: String { return "query error: \(q.query)" }
}
extension IOError: DatabaseError {
  var errorKind: String ( return "io error: \(i.filename)" }
}

extension Error {
  var databaseErrorKind: String {
    return (error as? DatabaseError)?.errorKind ?? "unexpected
non-database error"
  }
}

func queryDatabase(_ query: String) throws -> Table

do {
  queryDatabase("delete * from users")
} catch {
  os_log(error.databaseErrorKind)
}

This approach isn't sufficient for several reasons. Notably, it requires
the underlying errors to already have a distinct type for every category we
wish to place them in. If all network errors have the same type and I want
to categorize them based on network availability, authentication, dropped
connection, etc I am not able to do that.

Sorry, how does the presence or absence of typed throws play into this?

The kind of categorization I want to be able to do requires a custom
algorithm. The specific algorithm is used to categorize errors depends on
the dynamic context (i.e. the function that is propagating it). The way I
usually think about this categorization is as a conversion initializer as I
showed in the example, but it certainly wouldn't need to be accomplished
that way. The most important thing IMO is the ability to categorize during
error propagation and make information about that categorization easy for
callers to discover.

The output of the algorithm could use various mechanisms for
categorization - an enum is one mechanism, distinct types conforming to
appropriate categorization protocols is another. Attaching some kind of
category value to the original error or propagating the category along with
it might also work (although might be rather clunky).

It is trivial to make the original error immediately available via an
`underlyingError` property so I really don't understand the resistance to
wrapping errors. The categorization can easily be ignored at the catch
site if desired. That said, if we figure out some other mechanism for
categorizing errors, including placing different error values of the same
type into different categories, and matching them based on this
categorization I think I would be ok with that. Using wrapper types is not
essential to solving the problem.

Setting all of this aside, surely you had you had your own reasons for
supporting typed errors in the past. What were those and why do you no
longer consider them important?

My memory is certainly spotty, but as far as I can recall, I had no
distinct reasons; it just seemed like a reasonable and "natural" next step
that other people wanted for which I had no use case of my own in mind.
Having seen the argumentation that there aren't very many use cases in
general, I'm warming to the view that it's probably not such a great next
step.

···

On Fri, Aug 18, 2017 at 8:11 PM, Matthew Johnson <matthew@anandabits.com> wrote:

On Aug 18, 2017, at 6:56 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Fri, Aug 18, 2017 at 6:46 PM, Matthew Johnson <matthew@anandabits.com> > wrote:

On Aug 18, 2017, at 6:29 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Fri, Aug 18, 2017 at 6:19 PM, Matthew Johnson <matthew@anandabits.com> >> wrote:

On Aug 18, 2017, at 6:15 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Fri, Aug 18, 2017 at 09:20 Matthew Johnson via swift-evolution < >>> swift-evolution@swift.org> wrote:

Sent from my iPad

On Aug 18, 2017, at 1:27 AM, John McCall <rjmccall@apple.com> wrote:

>> On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution < >>>> swift-evolution@swift.org> wrote:
>> Splitting this off into its own thread:
>>
>>> On Aug 17, 2017, at 7:39 PM, Matthew Johnson < >>>> matthew@anandabits.com> wrote:
>>> One related topic that isn’t discussed is type errors. Many third
party libraries use a Result type with typed errors. Moving to an async /
await model without also introducing typed errors into Swift would require
giving up something that is highly valued by many Swift developers. Maybe
Swift 5 is the right time to tackle typed errors as well. I would be happy
to help with design and drafting a proposal but would need collaborators on
the implementation side.
>>
>> Typed throws is something we need to settle one way or the other,
and I agree it would be nice to do that in the Swift 5 cycle.
>>
>> For the purposes of this sub-discussion, I think there are three
kinds of code to think about:
>> 1) large scale API like Cocoa which evolve (adding significant
functionality) over the course of many years and can’t break clients.
>> 2) the public API of shared swiftpm packages, whose lifecycle may
rise and fall - being obsoleted and replaced by better packages if they
encounter a design problem.
>> 3) internal APIs and applications, which are easy to change because
the implementations and clients of the APIs are owned by the same people.
>>
>> These each have different sorts of concerns, and we hope that
something can start out as #3 but work its way up the stack gracefully.
>>
>> Here is where I think things stand on it:
>> - There is consensus that untyped throws is the right thing for a
large scale API like Cocoa. NSError is effectively proven here. Even if
typed throws is introduced, Apple is unlikely to adopt it in their APIs for
this reason.
>> - There is consensus that untyped throws is the right default for
people to reach for for public package (#2).
>> - There is consensus that Java and other systems that encourage
lists of throws error types lead to problematic APIs for a variety of
reasons.
>> - There is disagreement about whether internal APIs (#3) should use
it. It seems perfect to be able to write exhaustive catches in this
situation, since everything in knowable. OTOH, this could encourage abuse
of error handling in cases where you really should return an enum instead
of using throws.
>> - Some people are concerned that introducing typed throws would
cause people to reach for it instead of using untyped throws for public
package APIs.
>
> Even for non-public code. The only practical merit of typed throws I
have ever seen someone demonstrate is that it would let them use contextual
lookup in a throw or catch. People always say "I'll be able to
exhaustively switch over my errors", and then I ask them to show me where
they want to do that, and they show me something that just logs the error,
which of course does not require typed throws. Every. Single. Time.

I agree that exhaustive switching over errors is something that people
are extremely likely to actually want to do. I also think it's a bit of a
red herring. The value of typed errors is *not* in exhaustive switching.
It is in categorization and verified documentation.

Here is a concrete example that applies to almost every app. When you
make a network request there are many things that could go wrong to which
you may want to respond differently:
* There might be no network available. You might recover by updating
the UI to indicate that and start monitoring for a reachability change.
* There might have been a server error that should eventually be
resolved (500). You might update the UI and provide the user the ability
to retry.
* There might have been an unrecoverable server error (404). You will
update the UI.
* There might have been a low level parsing error (bad JSON, etc).
Recovery is perhaps similar in nature to #2, but the problem is less likely
to be resolved quickly so you may not provide a retry option. You might
also want to do something to notify your dev team that the server is
returning JSON that can't be parsed.
* There might have been a higher-level parsing error (converting JSON
to model types). This might be treated the same as bad JSON. On the other
hand, depending on the specifics of the app, you might take an alternate
path that only parses the most essential model data in hopes that the
problem was somewhere else and this parse will succeed.

All of this can obviously be accomplished with untyped errors. That
said, using types to categorize errors would significantly improve the
clarity of such code. More importantly, I believe that by categorizing
errors in ways that are most relevant to a specific domain a library
(perhaps internal to an app) can encourage developers to think carefully
about how to respond.

I used to be rather in favor of adding typed errors, thinking that it
can only benefit and seemed reasonable. However, given the very interesting
discussion here, I'm inclined to think that what you articulate above is
actually a very good argument _against_ adding typed errors.

If I may simplify, the gist of the argument advanced by Tino, Charlie,
and you is that the primary goal is documentation, and that documentation
in the form of prose is insufficient because it can be unreliable.
Therefore, you want a way for the compiler to enforce said documentation.
(The categorization use case, I think, is well addressed by the
protocol-based design discussed already in this thread.)

Actually documentation is only one of the goals I have and it is the
least important. Please see my subsequent reply to John where I articulate
the four primary goals I have for improved error handling, whether it be
typed errors or some other mechanism. I am curious to see what you think
of the goals, as well as what mechanism might best address those goals.

Your other three goals have to do with what you term categorization,
unless I misunderstand. Are those not adequately addressed by Joe Groff's
protocol-based design?

Can you elaborate on what you mean by Joe Gross’s protocol-based design?
I certainly haven’t seen anything that I believe addresses those goals well.

However, the compiler itself cannot reward, only punish in the form of
errors or warnings; if exhaustive switching is a red herring and the payoff
for typed errors is correct documentation, the effectiveness of this kind
of compiler enforcement must be directly proportional to the degree of
extrinsic punishment inflicted by the compiler (since the intrinsic reward
of correct documentation is the same whether it's spelled using doc
comments or the type system). This seems like a heavy-handed way to enforce
documentation of only one specific aspect of a throwing function; moreover,
if this use case were to be sufficiently compelling, then it's certainly a
better argument for SourceKit (or some other builtin tool) to automatically
generate information on all errors thrown than for the compiler to require
that users declare it themselves--even if opt-in.

Bad error handling is pervasive. The fact that everyone shows you code

that just logs the error is a prime example of this. It should be
considered a symptom of a problem, not an acceptable status quo to be
maintained. We need all the tools at our disposal to encourage better
thinking about and handling of errors. Most importantly, I think we need a
middle ground between completely untyped errors and an exhaustive list of
every possible error that might happen. I believe a well designed
mechanism for categorizing errors in a compiler-verified way can do exactly
this.

In many respects, there are similarities to this in the design of
`NSError` which provides categorization via the error domain. This
categorization is a bit more broad than I think is useful in many cases,
but it is the best example I'm aware of.

The primary difference between error domains and the kind of
categorization I am proposing is that error domains categorize based on the
source of an error whereas I am proposing categorization driven by likely
recovery strategies. Recovery is obviously application dependent, but I
think the example above demonstrates that there are some useful
generalizations that can be made (especially in an app-specific library),
even if they don't apply everywhere.

> Sometimes we then go on to have a conversation about wrapping errors
in other error types, and that can be interesting, but now we're talking
about adding a big, messy feature just to get "safety" guarantees for a
fairly minor need.

I think you're right that wrapping errors is tightly related to an
effective use of typed errors. You can do a reasonable job without
language support (as has been discussed on the list in the past). On the
other hand, if we're going to introduce typed errors we should do it in a
way that *encourages* effective use of them. My opinion is that
encouraging effect use means categorizing (wrapping) errors without
requiring any additional syntax beyond the simple `try` used by untyped
errors. In practice, this means we should not need to catch and rethrow an
error if all we want to do is categorize it. Rust provides good prior art
in this area.

>
> Programmers often have an instinct to obsess over error taxonomies
that is very rarely directed at solving any real problem; it is just
self-imposed busy-work.

I agree that obsessing over intricate taxonomies is counter-productive
and should be discouraged. On the other hand, I hope the example I
provided above can help to focus the discussion on a practical use of types
to categorize errors in a way that helps guide *thinking* and therefore
improves error handling in practice.

>
>> - Some people think that while it might be useful in some narrow
cases, the utility isn’t high enough to justify making the language more
complex (complexity that would intrude on the APIs of result types,
futures, etc)
>>
>> I’m sure there are other points in the discussion that I’m
forgetting.
>>
>> One thing that I’m personally very concerned about is in the systems
programming domain. Systems code is sort of the classic example of code
that is low-level enough and finely specified enough that there are lots of
knowable things, including the failure modes.
>
> Here we are using "systems" to mean "embedded systems and kernels".
And frankly even a kernel is a large enough system that they don't want to
exhaustively switch over failures; they just want the static guarantees
that go along with a constrained error type.
>
>> Beyond expressivity though, our current model involves boxing thrown
values into an Error existential, something that forces an implicit memory
allocation when the value is large. Unless this is fixed, I’m very
concerned that we’ll end up with a situation where certain kinds of systems
code (i.e., that which cares about real time guarantees) will not be able
to use error handling at all.
>>
>> JohnMC has some ideas on how to change code generation for ‘throws’
to avoid this problem, but I don’t understand his ideas enough to know if
they are practical and likely to happen or not.
>
> Essentially, you give Error a tagged-pointer representation to allow
payload-less errors on non-generic error types to be allocated globally,
and then you can (1) tell people to not throw errors that require
allocation if it's vital to avoid allocation (just like we would tell them
today not to construct classes or indirect enum cases) and (2) allow a
special global payload-less error to be substituted if error allocation
fails.
>
> Of course, we could also say that systems code is required to use a
typed-throws feature that we add down the line for their purposes. Or just
tell them to not use payloads. Or force them to constrain their error
types to fit within some given size. (Note that obsessive error taxonomies
tend to end up with a bunch of indirect enum cases anyway, because they get
recursive, so the allocation problem is very real whatever we do.)
>
> John.

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