[swift-evolution] [Discussion] Adopting a new common error type outside the bounds of NSError

Brent Royal-Gordon brent at architechies.com
Mon Mar 7 01:10:38 CST 2016

>> `code` is still magically synthesized, as indicated by the imaginary `@_numbered` attribute. I don't like that very much, but I don't see a good alternative to it, either—RawRepresentable synthesis *almost* works, except that cases with associated values wouldn't be supported.
> I disagree that this should be part of Error. `domain` and `code` are both relics of NSError integration and have no purpose when not bridging to NSError. The `domain` is really just a string representation of the error type, and the `code` is just an integral representation of the enum variant, so they're just imperfect representations of information already contained by the error value itself. We do actually need to include them in order to bridge any arbitrary Error to NSError, but they should stay as hidden implementation details (e.g. `_domain` and `_code`) like they are today. Besides, the names `domain` and `code` might be names that the concrete Error implementation actually wants to use for its own purposes.

To tell the truth, the early drafts of that email said that Error's requirements should be platform-dependent, but that the Swift compiler should always be able to synthesize all of them automatically. But as I thought about it more, I decided that probably wasn't right.

The reason I think that is Corelibs Foundation. It appears to me that the long-term plan for Swift is to have Foundation available in all environments Swift supports, except perhaps embedded ones. That means NSError is here to stay and interoperation between NSError and Swift.Error is going to be a permanent requirement.

> Also, there's no need to ever "unpack" userInfo. When you bridge a Swift error into NSError, it uses a private NSError subclass _SwiftNativeNSError, which boxes up the original error, and when that NSError is bridged back to ErrorType you get the original error value back automatically. Swift has an internal type _ObjectiveCBridgeableErrorType that has an init?(_bridgedNSError: NSError) method, but AIUI that is actually used to handle bridging of regular Cocoa NSErrors into the Swift ErrorType values that represent them (e.g. for all of the error enums found in the overlay module).

Yes, and I'm suggesting we should handle bidirectional bridging with public APIs, not private magic, and in a way that can expose all of the details to both sides. If NSError is a permanent part of Swift, I think we should plan to do that sooner or later.

> Really, the only thing we want to add to the current process is a way for an ErrorType to provide the .userInfo value of the bridged NSError, and I think we can do that just by adding a new protocol
> public protocol BridgedNSError {
>    var bridgedNSErrorUserInfo: [NSObject: AnyObject] { get }
> }
> (the property name is to avoid taking the generic term "userInfo", which the ErrorType might want to use for its own purposes)
> The machinery that bridges to NSError would simply test for conformance to this protocol and use it to provide the .userInfo if present.

I did think about moving the userInfo stuff (both the property and the initializer) into a separate protocol, but that was late in the drafting process and the email was long enough already.

>> CustomStringConvertible for Simple Error Messages
>> ----------------------------------------------------------------------
>> I think that we should encourage developers to conform errors with user-readable error messages to CustomStringConvertible and implement `description` to return that error. To that end, when a CustomStringConvertible Swift error is bridged to NSError, its `description` should become the NSError's `localizedDescription`. That ends up looking like this:
> I disagree here too. My CustomStringConvertible representation of my error may not be at all suitable for the localizedDescription of an NSError. The NSError's localizedDescription should be a string that is suitable for presenting to the user, but CustomStringConvertible isn't necessarily intended for showing to a user. For example, my enum's string representation may be "IOError(code: 3, path: "/tmp/foo")", which is certainly not what you want the user to see.

Primarily, I am suggesting that we should have *a standard protocol* for "give me a textual version of this instance that I can show to a user" and that we should, by convention, use that on `Error`s to convey error messages for errors that are suitable to present to users.

I am not sure if `CustomStringConvertible` is the right protocol for that purpose, but if it isn't—if `CustomStringConvertible` is meant to provide a programmer-readable representation—then frankly I'm not sure what `CustomDebugStringConvertible` is for. But the exact protocol used is immaterial.

(To be honest, a lot of the decisions around `CustomStringConvertible` confuse me; for example, I don't understand why Swift allows you to interpolate any instance into a string rather than only `CustomStringConvertible` instances. So this might just be part of the same blind spot.)

>> PresentableError
> I disagree here as well. If we have a way to provide the userInfo dictionary for a bridged NSError, then we don't need these fields (and in fact these fields would become useless as the bridging machinery wouldn't even look at them, seeing as how the error provides its own userInfo dictionary already).

The userInfo dictionary isn't always the source of this information; that's why NSError includes methods to retrieve them. The methods allow subclasses to override the defaults and generate error messages when they're needed, and NSError user info providers let you do the same thing without subclassing. Having bridged Swift errors call into particular methods on the Swift side would not be out of place.

> And I don't think the stdlib should try to define the semantics for how errors should be represented in a UI, since the stdlib doesn't do anything UI-related. We should leave this space open for other libraries to investigate alternatives to NSError's handling of this.

It may make sense to have the protocol for this be part of Foundation, but either way, I'd *really* like to have a very easy, clean way to move this stuff between Swift.Error-land and NSError-land.

> And FWIW, I don't think I've _ever_ seen anyone provide localizedRecoverySuggestion with NSError, and it's pretty rare to see a localizedFailureReason.

I think this may be why we're disagreeing about this so much—I suspect you use a very shallow subset of NSError's features, whereas I use them very aggressively. So let me explain where I'm coming from here.

In my current project, a mixed-language Mac app with about 30 errors, I have an NSError user info provider* that's about 300 lines long. Every error has a localizedDescription, most have a localizedRecoverySuggestion, and a perhaps a third to a half have a localizedFailureReason (mainly ones which end up getting wrapped in a generic "Document X cannot be opened" error; I append the localizedFailureReason to indicate what happened.) To support this, I of course have to pack a bunch of information into the `userInfo`; I have 14 keys I use for various purposes, and most of their values get inserted into error messages to make them more specific.

Structuring errors that thoroughly takes a lot of really boring code, but it pays off in really good error messages and diagnostics. I can usually pass an error straight into `-presentError:` with little to no modification.

(I don't know how you *wouldn't* use a `localizedRecoverySuggestion`, honestly; those provide the small text in an error dialog.)

* User info providers are a new feature not yet listed in the documentation, but there are doc comments for them. They basically let you specify a block that will fill in missing user info keys for a particular error domain, so you can centralize error message generation. See `-[NSError setUserInfoValueProviderForDomain:provider:]`.

>> RecoverableError
> Just like PresentableError, I don't think this is appropriate for the Swift stdlib.

I've generally found the NSError equivalent to be a great feature marred by a fairly clumsy design. (If it had been built post-blocks it would be far better.) But you may be right that it's higher-level than what stdlib should model.

Brent Royal-Gordon

More information about the swift-evolution mailing list