[swift-evolution] typed throws

Charlie Monroe charlie at charliemonroe.net
Fri Aug 18 03:58:44 CDT 2017


> On Aug 18, 2017, at 10:22 AM, John McCall <rjmccall at apple.com> wrote:
> 
>> On Aug 18, 2017, at 3:28 AM, Charlie Monroe <charlie at charliemonroe.net <mailto:charlie at charliemonroe.net>> wrote:
>>> On Aug 18, 2017, at 8:27 AM, John McCall via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>> On Aug 18, 2017, at 12:58 AM, Chris Lattner via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>> Splitting this off into its own thread:
>>>> 
>>>>> On Aug 17, 2017, at 7:39 PM, Matthew Johnson <matthew at anandabits.com <mailto:matthew at 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.

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.



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

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170818/bfb60d31/attachment.html>


More information about the swift-evolution mailing list