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

Colin Cornaby colin.cornaby at mac.com
Mon Dec 21 17:36:49 CST 2015


So here’s my 2 cents. I talked to Charles about this off list, so I’ll just say my piece and then move along. :)

It seems to me that ErrorType is very empty because it’s meant to be a placeholder in the language for whatever the native error type is on the platform. When I’m in AppKit/UIKit/OSS Swift Foundation, I’m using NSError. When I’m using C API, my ErrorType would probably be a typedef’d int. When I’m using Win32, my ErrorType is whatever Win32 error types are. If I’m using Swift with some platform/API where the error types are some sort of struct, or even a string type, those types can be made to conform to error type. It’s meant to be a shallow wrapper because it’s just a placeholder to wrap the native platform API.

I’m not sure I like going down the path of Swift actually trying to replace platform API, especially if it requires a hand bridge. Then you’re constantly extending the language to bridge the error types from all the different platforms. My view is that OSS Swift Foundation provides a template where the platform API cannot be assumed, and that OSS Swift Foundation defines it’s own concrete error type in NSError/Error. Certainly NSError isn’t perfect and the conversation on how to make it better is worthwhile (especially now that NSError is part of Swift Foundation, and changes could also be adopted by Obj-C Foundation where relevant.)

A year ago my opinion on this was very different, and I was bothering people on the Swift team for a similar improvement based on where Swift was at the time. But with OSS Foundation and multiplatform Swift my feeling has changed. I’m not comfortable with trying to promote NSError concepts higher into the language when I think Swift conforming to the host platform’s error API is the better path. My assumption is that Swift is not meant to be a platform in itself (like Java), but just a language over an existing platform.

> On Dec 19, 2015, at 9:00 PM, Dennis Lysenko via swift-evolution <swift-evolution at swift.org> 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 <mailto: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 <mailto:swift-evolution at swift.org>
> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
>  _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151221/8c4c3fca/attachment.html>


More information about the swift-evolution mailing list