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

Kevin Ballard kevin at sb.org
Sun Mar 6 22:51:25 CST 2016


On Sun, Mar 6, 2016, at 06:38 PM, Brent Royal-Gordon via swift-evolution wrote:
> > I wouldn’t want a single, universal error type for the standard library. I would want to be able to selectively catch errors based on their type rather than having to look into the “reason” string to determine what happened. (In other words, the reason should be encoded in the error type.)
> 
> I agree. A single concrete error type with stringly-typed data inside it is the precise *opposite* of how the error mechanism should be used.
> 
> Swift errors use types and cases to clearly categorize errors and make them easy to programmatically match; they then use associated values to explicitly model the details of each type of error, and pattern matching to allow `catch` clauses to easily zero in on specific errors they can handle, no matter how finicky the requirements. When you start throwing around concrete `Error` structs with `reason` strings and unstructured bags of data called `infoDictionary`, you severely handicap every one of those features.
> 
> In general, I think this is tackling the wrong problem. Each error type can manage its own storage containing whatever that type happens to need. Swift will automatically copy or move that information around as appropriate. Simply put, there's nothing we need to do there.
> 
> There are, however, two broad categories of things I *do* think should be modeled better:
> 
> - The mapping between a Swift Error and an NSError.
> - The way errors are presented to the user.
> 
> So here's what I suggest we do to Error in Swift 3.
> 
> Clarify and Formalize NSError Bridging
> ----------------------------------------------------
> 
> Currently, the only requirements of `Error` are the hidden `_domain` and `_code` types. Let's formalize those. We'll also add support for `userInfo`, and include a constructor so we can unpack `userInfo` in the other direction.
> 
> 	public protocol Error {
> 		var domain: String { get }
> 		@_numbered var code: Int { get }
> 		var userInfo: [String: AnyObject] { get }
> 		
> 		init(code: Int, userInfo: [String: AnyObject])
> 	}
> 
> 	extension Error {
> 		public var domain: String {
> 			return String(reflecting: self)
> 		}
> 		
> 		public var userInfo: [String: AnyObject] {
> 			return [:]
> 		}
> 		
> 		@_numbered init(code: Int, userInfo: [String: AnyObject]) {
> 			self.init(_numbered: code)
> 		}
> 	}
> 	
> 	public extension Any<Error> {
> 		public init(_: NSError) { ...omitted... }
> 	}
> 	
> 	public extension NSError {
> 		public init(_: Error) { ...omitted... }
> 	}
> 
> `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.

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).

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.

And of course this protocol would live in the Foundation overlay instead of being part of the stdlib proper.

> 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:
> 
> 	enum HollywoodCarError: Error, CustomStringConvertible {
> 		case exploded
> 		case engineFellOut
> 		
> 		var description: String {
> 			switch self {
> 			case .exploded:
> 				return localized("The car has exploded in a fireball.")
> 			case .engineFellOut:
> 				return localized("The car's engine has fallen out.")
> 			}
> 		}
> 	}
> 
> (The `CustomStringConvertible` documentation is not entirely clear on whether `description` should be user-readable and localized. I interpret it to be "yes", but if not, we can provide a `LocalizedStringConvertible` protocol along the same lines. In either case, the point is that this is *not* a feature limited to errors; there really ought to be a protocol for anything which can be shown to users in a plain-text form.)

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.

> PresentableError
> -----------------------
> 
> There are two other parts of NSError that I think we should widely model because they'd be useful on all platforms.
> 
> One is the ability to provide a more detailed error message. NSError calls this the `localizedRecoverySuggestion`; I recommend we just call it `suggestion` or perhaps `details`.
> 
> The other is the ability to retrieve a terse sentence fragment explaining the error's cause, which can be inserted into a larger paragraph. NSError calls this `localizedFailureReason`; I suggest simply using `reason`.
> 
> Both of these fields should be bridged into their NSError equivalents, and conforming to their protocol would also imply conformance to `CustomStringConvertible`.

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). 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.

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

> RecoverableError
> -----------------------
> 
> Lower-priority because it's not used *that* often, but I think it's worth modeling in the long run. It may make sense to put this in Foundation instead of stdlib, though.
> 
> The idea behind Cocoa's `recoveryAttempter` is good, but I think the design is a problem, especially in Corelibs Foundation where we won't be able to perform selectors. So I suggest an alternative design. I believe it can be bridged to NSError's approach, although it would not quite be a *trivial* bridging process.
> 
> First, the RecoveryAttempting protocol, which models one particular approach to recovering from the error:
> 
> 	// This is class-constrained in case `recover(from:completion:)` needs to be mutating.
> 	public protocol RecoveryAttempting: class, CustomStringConvertible {
> 		// The CustomStringConvertible name should be suitable for buttons.
> 		
> 		var destructive: Bool
> 		
> 		/// Attempt to recover from the error using this attempter. Calls `completion` when finished.
> 		func recover(from error: Error, completion: (Bool, ErrorType?) -> Void)
> 	}
> 	extension RecoveryAttempting {
> 		public var destructive: Bool { return false }
> 	}
> 
> Second, the RecoverableError protocol:
> 
> 	public protocol RecoverableError: Error {
> 		var recoveryAttempters: [Any<RecoveryAttempting>] { get }
> 	}
> 
> We should document that you should only provide non-trivial recovery attempters (i.e. no Cancel options). Higher-level error presentation code can always do that.

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

-Kevin Ballard


More information about the swift-evolution mailing list