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

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


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/3e02bad6/attachment.html>


More information about the swift-evolution mailing list