[swift-evolution] [Proposal idea] Improved interop for ErrorType->NSError

Dennis Lysenko dennis.s.lysenko at gmail.com
Sat Dec 19 23:03:45 CST 2015


Sorry, got a little excited in answering and just re-read the proposal.
Ignore the part of "extra cruft", since I'll never have to see it unless I
want to override it. I thought that the static dispatch bit later on in the
proposal implies that that wouldn't work so well, but I might have misread.

On Sun, Dec 20, 2015 at 12:00 AM Dennis Lysenko <dennis.s.lysenko at gmail.com>
wrote:

> Charles,
>
> While I agree it's unfortunate that there isn't much interop between
> ErrorType and NSError, the decision from the core team to make ErrorType an
> empty protocol seems to have been a premeditated one. I would love to have
> someone on the core team respond here in my stead, but I'll try and present
> my side of it.
>
> I have a lot of errors that come up internally in my app's workings. I
> subscribe to Swift's error handling rationale regarding recoverable errors.
> For example, my internal networking library has a NoInternetError which
> never needs to be presented using built-in cocoa error presentation, but
> instead has a different presentation scheme for each different context
> (e.g. if it happened while loading a tableview, it shows up as the
> placeholder text. if it happened in response to a user action, it displays
> an alert.) I, therefore, strongly disagree with adding any extra members to
> the ErrorType protocol: it's cruft that will never be of use to me.
>
> Now, this is the part that I'm not sure about and I might be corrected by
> someone from the core team: It seems that part of the rationale for
> introducing the general ErrorType is to move *away* from NSError. NSError
> is antiquated and carries information that is often not even used (take a
> survey of 100 swift programmers, and maybe 40 of them will care to tell you
> what an error domain is).
>
> A lot of this probably hinges on typed `throws` annotations as well; if we
> get those, then you never have to catch general ErrorType unless you wrote
> a function to throw general ErrorType, so you could make all your error
> types conform to a custom protocol with all this NSError-conformant info in
> it without worrying about static dispatch.
>
> On Sat, Dec 19, 2015 at 10:50 PM Charles Srstka via swift-evolution <
> swift-evolution at swift.org> wrote:
>
>> This is a bit of a pre-proposal; I wanted to bounce some ideas off the
>> community before writing up a formal proposal, to see how people felt about
>> this.
>>
>> The Problem:
>>
>> Swift introduces the very nifty ErrorType protocol, which, if implemented
>> as an enum, allows one to associate arguments with the specific error cases
>> with which they are relevant, as in this example:
>>
>> enum MyError: ErrorType {
>>     case JustFouledUp
>>     case CouldntDealWithFile(url: NSURL)
>>     case CouldntDealWithSomeValue(value: Int)
>> }
>>
>> This is great, because it ensures that the file-related error will always
>> have a URL object, whereas it won’t be included in the cases where it is
>> irrelevant.
>>
>> Unfortunately, the Cocoa APIs needed to display an error object to the
>> user all take NSErrors, and the conversion from other ErrorTypes to
>> NSErrors is not very good; using “as NSError” to convert a MyError will
>> result in something that has all of the associated values removed, and the
>> message displayed to the user will be a cryptic and not particularly
>> user-friendly “MyError error 1” rather than something more descriptive. One
>> can define an “toNSError()” method on one’s own error type that will
>> properly propagate the NSError’s userInfo dictionary such that it will be
>> displayed in a more meaningful way:
>>
>> enum MyError: ErrorType {
>>     case JustFouledUp
>>     case CouldntDealWithFile(url: NSURL)
>>     case CouldntDealWithSomeValue(value: Int)
>>
>>     func toNSError() -> NSError {
>>         var userInfo = [String : AnyObject]()
>>         let code: Int
>>
>>
>>         switch self {
>>         case .JustFouledUp:
>>             userInfo[NSLocalizedFailureReasonErrorKey] = "Something
>> fouled up!"
>>             code = 0
>>         case let .CouldntDealWithFile(url):
>>             userInfo[NSLocalizedFailureReasonErrorKey] = "Something went
>> wrong with the file \(url.lastPathComponent ?? "(null)")."
>>             userInfo[NSURLErrorKey] = url
>>             code = 1
>>         case let .CouldntDealWithSomeValue(value):
>>             userInfo[NSLocalizedFailureReasonErrorKey] = "This value
>> isn't legit for some reason: \(value)"
>>             code = 2
>>         }
>>
>>
>>         return NSError(domain: "MyError", code: code, userInfo: userInfo)
>>     }
>> }
>>
>> However, since this method will only be invoked if called explicitly, one
>> has to make sure to include .toNSError() every time when throwing an error
>> object, or else it will not display properly; one can never just throw a
>> MyError object. This is error-prone (no pun intended), and also prevents
>> things like making the error conform to Equatable and comparing it against
>> an ErrorType we received from some other method.
>>
>> I propose an addition to the ErrorType protocol, provisionally entitled
>> “toNSError()”, but which another name could be given if something else is
>> determined to be more appropriate. This method would be called by the
>> system whenever “as NSError” is called on something that implements
>> ErrorType. While this method would be added to the protocol, a default
>> implementation would be provided in an extension, so that implementers of
>> ErrorType would never actually need to override it unless very specific
>> customization was required. For this default implementation, several other
>> properties corresponding to some of the more commonly-used NSError keys
>> (defined with default implementations returning nil) would be defined and
>> referenced, and anything that returned non-nil would be packaged into a
>> userInfo dictionary, like so:
>>
>> protocol ErrorType {
>>     var description: String? { get }
>>     var failureReason: String? { get }
>>     var recoverySuggestion: String? { get }
>>     var recoveryOptions: [String]? { get }
>>     var recoveryAttempter: AnyObject? { get }
>>     var helpAnchor: String? { get }
>>     var underlyingError: ErrorType? { get }
>>     var URL: NSURL? { get }
>>
>>
>>     func toNSError() -> NSError
>> }
>>
>> extension ErrorType {
>>     var description: String? { return nil }
>>     var failureReason: String? { return nil }
>>     var recoverySuggestion: String? { return nil }
>>     var recoveryOptions: [String]? { return nil }
>>     var recoveryAttempter: AnyObject? { return nil }
>>     var helpAnchor: String? { return nil }
>>     var underlyingError: ErrorType? { return nil }
>>     var URL: NSURL? { return nil }
>>
>>
>>     func toNSError() -> NSError {
>>         let domain = // do what “as NSError” currently does to generate
>> the domain
>>         let code = // do what “as NSError” currently does to generate
>> the code
>>
>>
>>         var userInfo = [String : AnyObject]()
>>
>>
>>         if let description = self.description {
>>             userInfo[NSLocalizedDescriptionKey] = description
>>         }
>>
>>         if let failureReason = self.failureReason {
>>             userInfo[NSLocalizedFailureReasonErrorKey] = failureReason
>>         }
>>
>>
>>         if let suggestion = self.recoverySuggestion {
>>             userInfo[NSLocalizedRecoverySuggestionErrorKey] = suggestion
>>         }
>>
>>
>>         if let options = self.recoveryOptions {
>>             userInfo[NSLocalizedRecoveryOptionsErrorKey] = options
>>         }
>>
>>
>>         if let attempter = self.recoveryAttempter {
>>             userInfo[NSRecoveryAttempterErrorKey] = attempter
>>         }
>>
>>         if let anchor = self.helpAnchor {
>>             userInfo[NSHelpAnchorErrorKey] = anchor
>>         }
>>
>>
>>         if let underlying = self.underlyingError {
>>             userInfo[NSUnderlyingErrorKey] = underlying.toNSError()
>>         }
>>
>>
>>         if let url = self.URL {
>>             userInfo[NSURLErrorKey] = url
>>
>>
>>             if url.fileURL, let path = url.path {
>>                 userInfo[NSFilePathErrorKey] = path
>>             }
>>         }
>>
>>
>>         return NSError(domain: domain, code: code, userInfo: userInfo)
>>     }
>> }
>>
>> Thanks to all the default implementations, the error type would only have
>> to implement the properties corresponding to the userInfo keys that the
>> implementer deems relevant, as in:
>>
>> enum MyError: ErrorType {
>>     case JustFouledUp
>>     case CouldntDealWithFile(url: NSURL)
>>     case CouldntDealWithSomeValue(value: Int)
>>
>>
>>     var failureReason: String? {
>>         switch self {
>>         case .JustFouledUp:
>>             return "Something fouled up!"
>>         case let .CouldntDealWithFile(url):
>>             return "Something went wrong with the file \(url.
>> lastPathComponent ?? "(null)")."
>>         case let .CouldntDealWithSomeValue(value):
>>             return "This value isn't legit for some reason: \(value)"
>>         }
>>     }
>>
>>
>>     var URL: NSURL? {
>>         switch self {
>>         case let .CouldntDealWithFile(url):
>>             return url
>>         default:
>>             return nil
>>         }
>>     }
>> }
>>
>> This could then be created and passed to an API taking an NSError like so:
>>
>> let err = MyError.CouldntDealWithFile(url: NSURL(fileURLWithPath:
>> "/path/to/file"))
>>
>> NSApp.presentError(err as NSError)
>>
>> and everything would be properly presented to the user.
>>
>> Similar functionality could be added to the protocol for conversions in
>> the other direction, although this would be more difficult and would
>> require more work on the implementer’s part.
>>
>> The biggest problem I see to the idea is the use of references to
>> Foundation types—NSError and NSURL—in the ErrorType definition, which may
>> be undesired in a pure-Swift environment. In particular, usage of the NSURL
>> type for the ‘URL’ property, which could have useful applications outside
>> of simple Obj-C interop, could be irksome. Normally I would just propose
>> adding things in an extension, but of course in this case, declaring
>> methods in protocol extensions causes them to be statically dispatched,
>> which could result in the wrong methods being called if the caller thought
>> it was looking at a generic ErrorType rather than the specific concrete
>> type. Perhaps this could spark a new discussion on whether there ought to
>> be a way to declare dynamically-dispatched methods in protocol extensions.
>> It’s also possible that Swift could use a built-in URL type, equivalent to
>> Foundation’s NSURL, eliminating the need for any NS types other than
>> NSError here. It’s also possible that since there appears to be an
>> open-source implementation of Foundation in the github repository, that
>> this isn’t even an issue and is something we can just leave in. At any
>> rate, I thought this might be an interesting starting point for discussion.
>>
>> Of course, an alternative solution could be to define “domain”, “code”,
>> and “userInfo” properties (I know the first two are already in there, but
>> this would make them public) in the protocol and just use those. These
>> could also get default implementations that would work similarly to what is
>> above, which, if Swift gained a native URL type, could completely eliminate
>> Foundation types from the public interface.
>>
>> What do you think?
>>
>> Charles
>>
>> _______________________________________________
>> swift-evolution mailing list
>> 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/20151220/5f741681/attachment.html>


More information about the swift-evolution mailing list