[Proposal draft] NSError bridging


(Douglas Gregor) #1

Hi all,

Proposal link: https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md

Here is a detailed proposal draft for bridging NSError to ErrorProtocol. Getting this right is surprisingly involved, so the detailed design on this proposal is fairly large. Comments welcome!

  - Doug

NSError Bridging

Proposal: SE-NNNN <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/NNNN-nserror-bridging.md>
Author: Doug Gregor <https://github.com/DougGregor>, Charles Srstka <https://github.com/CharlesJS>
Status: Awaiting review
Review manager: TBD
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#introduction>Introduction

Swift's error handling model interoperates directly with Cocoa's NSError conventions. For example, an Objective-C method with an NSError** parameter, e.g.,

- (nullable NSURL *)replaceItemAtURL:(NSURL *)url options:(NSFileVersionReplacingOptions)options error:(NSError **)error;
will be imported as a throwing method:

func replaceItem(at url: URL, options: ReplacingOptions = []) throws -> URL
Swift bridges between ErrorProtocol-conforming types and NSError so, for example, a Swift enum that conforms toErrorProtocol can be thrown and will be reflected as an NSError with a suitable domain and code. Moreover, an NSErrorproduced with that domain and code can be caught as the Swift enum type, providing round-tripping so that Swift can deal in ErrorProtocol values while Objective-C deals in NSError objects.

However, the interoperability is incomplete in a number of ways, which results in Swift programs having to walk a careful line between the ErrorProtocol-based Swift way and the NSError-based way. This proposal attempts to bridge those gaps.

Swift-evolution thread: Charles Srstka's pitch for Consistent bridging for NSErrors at the language boundary <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160502/016618.html>, which discussed Charles' original proposal <https://github.com/apple/swift-evolution/pull/331> that addressed these issues by providing NSError to ErrorProtocol bridging and exposing the domain, code, and user-info dictionary for all errors. This proposal expands upon that work, but without directly exposing the domain, code, and user-info.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#motivation>Motivation

There are a number of weaknesses in Swift's interoperability with Cocoa's error model, including:

There is no good way to provide important error information when throwing an error from Swift. For example, let's consider a simple application-defined error in Swift:

enum HomeworkError : Int, ErrorProtocol {
  case forgotten
  case lost
  case dogAteIt
}
One can throw HomeworkError.dogAteIt and it can be interpreted as an NSError by Objective-C with an appropriate error domain (effectively, the mangled name of the HomeworkError type) and code (effectively, the case discriminator). However, one cannot provide a localized description, help anchor, recovery attempter, or any other information commonly placed into the userInfo dictionary of an NSError. To provide these values, one must specifically construct an NSError in Swift, e.g.,

throw NSError(code: HomeworkError.dogAteIt.rawValue,
              domain: HomeworkError._domain,
              userInfo: [ NSLocalizedDescriptionKey : "the dog ate it" ])
There is no good way to get information typically associated with NSError's userInfo in Swift. For example, the Swift-natural way to catch a specific error in the AVError error domain doesn't give one access to the userInfo dictionary, e.g.,:

catch let error as AVError where error == .diskFull {
  // AVError is an enum, so one only gets the equivalent of the code.
  // There is no way to access the localized description (for example) or
  // any other information typically stored in the ``userInfo`` dictionary.
}
The workaround is to catch as an NSError, which is quite a bit more ugly:

catch let error as NSError where error._domain == AVFoundationErrorDomain && error._code == AVFoundationErrorDomain.diskFull.rawValue {
  // okay: userInfo is finally accessible, but still weakly typed
}
This makes it extremely hard to access common information, such as the localized description. Moreover, the userInfo dictionary is effectively untyped so, for example, one has to know a priori that the value associated with the known AVErrorDeviceKey will be typed as CMTime:

catch let error as NSError where error._domain = AVFoundationErrorDomain {
  if let time = error.userInfo[AVErrorDeviceKey] as? CMTime {
    // ...
  }
}
It would be far better if one could catch an AVError directly and query the time in a type-safe manner:

catch let error as AVError {
  if let time = error.time {
    // ...
  }
}
NSError is inconsistently bridged with ErrorProtocol. Swift interoperates by translating between NSError and ErrorProtocol when mapping between a throwing Swift method/initializer and an Objective-C method with an NSError** parameter. However, an Objective-C method that takes an NSError* parameter (e.g., to render it) does not bridge to ErrorProtocol, meaning that NSError is part of the API in Swift in some places (but not others). For example, NSError leaks through when the following UIDocument API in Objective-C:

- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
is imported into Swift as follows:

func handleError(_ error: NSError, userInteractionPermitted: Bool)
One would expect the first parameter to be imported as ErrorProtocol.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#proposed-solution>Proposed solution

This proposal involves directly addressing (1)-(3) with new protocols and a different way of bridging Objective-C error code types into Swift, along with some conveniences for working with Cocoa errors:

Introduce three new protocols for describing more information about errors: LocalizedError, RecoverableError, andCustomNSError. For example, an error type can provide a localized description by conforming to LocalizedError:

extension HomeworkError : LocalizedError {
  var errorDescription: String? {
    switch self {
    case .forgotten: return NSLocalizedString("I forgot it")
    case .lost: return NSLocalizedString("I lost it")
    case .dogAteIt: return NSLocalizedString("The dog ate it")
    }
  }
}
Imported Objective-C error types should be mapped to struct types that store an NSError so that no information is lost when bridging from an NSError to the Swift error types. We propose to introduce a new macro, NS_ERROR_ENUM, that one can use to both declare an enumeration type used to describe the error codes as well as tying that type to a specific domain constant, e.g.,

typedef NS_ERROR_ENUM(NSInteger, AVError, AVFoundationErrorDomain) {
  AVErrorUnknown = -11800,
  AVErrorOutOfMemory = -11801,
  AVErrorSessionNotRunning = -11803,
  AVErrorDeviceAlreadyUsedByAnotherSession = -11804,
  // ...
}
The imported AVError will have a struct that allows one to access the userInfo dictionary directly. This retains the ability to catch via a specific code, e.g.,

catch AVError.outOfMemory {
// ...
}
However, catching a specific error as a value doesn't lose information:

catch let error as AVError where error.code == .sessionNotRunning {
// able to access userInfo here!
}
This also gives the ability for one to add typed accessors for known keys within the userInfo dictionary:

extension AVError {
var time: CMTime? {
   get {
     return userInfo[AVErrorTimeKey] as? CMTime?
   }

   set {
     userInfo[AVErrorTimeKey] = newValue.map { $0 as CMTime }
   }
}
}
Bridge NSError to ErrorProtocol, so that all NSError uses are bridged consistently. For example, this means that the Objective-C API:

- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
is imported into Swift as:

func handleError(_ error: ErrorProtocol, userInteractionPermitted: Bool)
This will use the same bridging logic in the Clang importer that we use for other value types (Array, String, URL, etc.), but with the runtime translation we've already been doing for catching/throwing errors.

When we introduce this bridging, we will need to remove NSError's conformance to ErrorProtocol to avoid creating cyclic implicit conversions. However, we still need an easy way to create an ErrorProtocol instance from an arbitrary NSError, e.g.,

extension NSError {
  var asError: ErrorProtocol { ... }
}
In Foundation, add an extension to ErrorProtocol that provides typed access to the common user-info keys. Note that we focus only on those user-info keys that are read by user code (vs. only accessed by frameworks):

extension ErrorProtocol {
  // Note: for exposition only. Not actual API.
  private var userInfo: [NSObject : AnyObject] {
    return (self as! NSError).userInfo
  }

  var localizedDescription: String {
    return (self as! NSError).localizedDescription
  }

  var filePath: String? {
    return userInfo[NSFilePathErrorKey] as? String
  }

  var stringEncoding: String.Encoding? {
    return (userInfo[NSStringEncodingErrorKey] as? NSNumber)
             .map { String.Encoding(rawValue: $0.uintValue) }
  }

  var underlying: ErrorProtocol? {
    return (userInfo[NSUnderlyingErrorKey] as? NSError)?.asError
  }

  var url: URL? {
    return userInfo[NSURLErrorKey] as? URL
  }
}
Rename ErrorProtocol to Error: once we've completed the bridging story, Error becomes the primary way to work with error types in Swift, and the value type to which NSError is bridged:

func handleError(_ error: Error, userInteractionPermitted: Bool)
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#detailed-design>Detailed design

This section details both the design (including the various new protocols, mapping from Objective-C error code enumeration types into Swift types, etc.) and the efficient implementation of this design to interoperate with NSError. Throughout the detailed design, we already assume the name change from ErrorProtocol to Error.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#new-protocols>New protocols

This proposal introduces several new protocols that allow error types to expose more information about error types.

The LocalizedError protocol describes an error that provides localized messages for display to the end user, all of which provide default implementations. The conforming type can provide implementations for any subset of these requirements:

protocol LocalizedError : Error {
  /// A localized message describing what error occurred.
  var errorDescription: String? { get }

  /// A localized message describing the reason for the failure.
  var failureReason: String? { get }

  /// A localized message describing how one might recover from the failure.
  var recoverySuggestion: String? { get }

  /// A localized message providing "help" text if the user requests help.
  var helpAnchor: String? { get }
}

extension LocalizedError {
  var errorDescription: String? { return nil }
  var failureReason: String? { return nil }
  var recoverySuggestion: String? { return nil }
  var helpAnchor: String? { return nil }
}
The RecoverableError protocol describes an error that might be recoverable:

protocol RecoverableError : Error {
  /// Provides a set of possible recovery options to present to the user.
  var recoveryOptions: [String] { get }

  /// Attempt to recover from this error when the user selected the
  /// option at the given index. This routine must call resultHandler and
  /// indicate whether recovery was successful (or not).
  ///
  /// This entry point is used for recovery of errors handled at a
  /// "document" granularity, that do not affect the entire
  /// application.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int,
                       andThen resultHandler: (recovered: Bool) -> Void)

  /// Attempt to recover from this error when the user selected the
  /// option at the given index. Returns true to indicate
  /// successful recovery, and false otherwise.
  ///
  /// This entry point is used for recovery of errors handled at
  /// the "application" granularity, where nothing else in the
  /// application can proceed until the attmpted error recovery
  /// completes.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool
}

extension RecoverableError {
  /// By default, implements document-modal recovery via application-model
  /// recovery.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int,
                       andThen resultHandler: (Bool) -> Void) {
    resultHandler(recovered: attemptRecovery(optionIndex: recoveryOptionIndex))
  }
}
Error types that conform to RecoverableError may be given an opportunity to recover from the error. The user can be presented with some number of (localized) recovery options, described by recoveryOptions, and the selected option will be passed to the appropriate attemptRecovery method.

The CustomNSError protocol describes an error that wants to provide custom NSError information. This can be used, e.g., to provide a specific domain/code or to populate NSError's userInfo dictionary with values for custom keys that can be accessed from Objective-C code but are not covered by the other protocols.

/// Describes an error type that fills in the userInfo directly.
protocol CustomNSError : Error {
  var errorDomain: String { get }
  var errorCode: Int { get }
  var errorUserInfo: [String : AnyObject] { get }
}
Note that, unlike with NSError, the provided errorUserInfo requires String keys. This is in line with common practice for NSError and is important for the implementation (see below). All of these properties are defaulted, so one can provide any subset:

extension CustomNSError {
  var errorDomain: String { ... }
  var errorCode: Int { ... }
  var errorUserInfo: [String : AnyObject] { ... }
}
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#mapping-error-types-to-nserror>Mapping error types to NSError

Every type that conforms to the Error protocol is implicitly bridged to NSError. This has been the case since Swift 2, where the compiler provides a domain (i.e., the mangled name of the type) and code (based on the discriminator of the enumeration type). This proposal also allows for the userInfo dictionary to be populated by the runtime, which will check for conformance to the various protocols (LocalizedError, RecoverableError, or CustomNSError) to retrieve information.

Conceptually, this could be implemented by eagerly creating a userInfo dictionary for a given instance of Error:

func createUserInfo(error: Error) -> [NSObject : AnyObject] {
  var userInfo: [NSObject : AnyObject] = [:]

  // Retrieve custom userInfo information.
  if let customUserInfoError = error as? CustomNSError {
    userInfo = customUserInfoError.userInfo
  }

  if let localizedError = error as? LocalizedError {
    if let description = localizedError.errorDescription {
      userInfo[NSLocalizedDescriptionKey] = description
    }

    if let reason = localizedError.failureReason {
      userInfo[NSLocalizedFailureReasonErrorKey] = reason
    }

    if let suggestion = localizedError.recoverySuggestion {
      userInfo[NSLocalizedRecoverySuggestionErrorKey] = suggestion
    }

    if let helpAnchor = localizedError.helpAnchor {
      userInfo[NSHelpAnchorErrorKey] = helpAnchor
    }
  }

  if let recoverableError = error as? RecoverableError {
    userInfo[NSLocalizedRecoveryOptionsErrorKey] = recoverableError.recoveryOptions
    userInfo[NSRecoveryAttempterErrorKey] = RecoveryAttempter()
  }
}
The RecoveryAttempter class is an implementation detail. It will implement the informal protocol NSErrorRecoveryAttempting <https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Protocols/NSErrorRecoveryAttempting_Protocol/> for the given error:

class RecoveryAttempter : NSObject {
  @objc(attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo:)
  func attemptRecovery(fromError nsError: NSError,
                       optionIndex recoveryOptionIndex: Int,
                       delegate: AnyObject?,
                       didRecoverSelector: Selector,
                       contextInfo: UnsafeMutablePointer<Void>) {
    let error = nsError as! RecoverableError
    error.attemptRecovery(optionIndex: recoveryOptionIndex) { success in
      // Exposition only: this part will actually have to be
      // implemented in Objective-C to pass the BOOL and void* through.
      delegate?.perform(didRecoverSelector, with: success, with: contextInfo)
    }
  }

  @objc(attemptRecoveryFromError:optionIndex:)
  func attemptRecovery(fromError nsError: NSError,
                       optionIndex recoveryOptionIndex: Int) -> Bool {
    let error = nsError as! RecoverableError
    return error.attemptRecovery(optionIndex: recoveryOptionIndex)
  }
}
The actual the population of the userInfo dictionary should not be eager. NSError provides the notion of global "user info value providers" that it uses to lazily request the values for certain keys, via setUserInfoValueProvider(forDomain:provider:), which is declared as:

extension NSError {
  @available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *)
  class func setUserInfoValueProvider(forDomain errorDomain: String,
                                      provider: ((NSError, String) -> AnyObject?)? = nil)
}
The runtime would need to register a user info value provider for each error type the first time it is bridged into NSError, supplying the domain and the following user info value provider function:

func userInfoValueProvider(nsError: NSError, key: String) -> AnyObject? {
  let error = nsError as! Error
  switch key {
  case NSLocalizedDescriptionKey:
    return (error as? LocalizedError)?.errorDescription

  case NSLocalizedFailureReasonErrorKey:
    return (error as? LocalizedError)?.failureReason

  case NSLocalizedRecoverySuggestionErrorKey:
    return (error as? LocalizedError)?.recoverySuggestion

  case NSHelpAnchorErrorKey:
    return (error as? LocalizedError)?.helpAnchor

  case NSLocalizedRecoveryOptionsErrorKey:
    return (error as? RecoverableError)?.recoveryOptions

  case NSRecoveryAttempterErrorKey:
    return error is RecoverableError ? RecoveryAttempter() : nil

  default:
    guard let customUserInfoError = error as? CustomNSError else { return nil }
    return customUserInfoError.userInfo[key]
  }
}
On platforms that predate the introduction of user info value providers, there are alternate implementation strategies, including introducing a custom NSDictionary subclass to use as the userInfo in the NSError that lazily populates the dictionary by, effectively, calling the userInfoValueProvider function above for each requested key. Or, one could eagerly populate userInfo on older platforms.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#importing-error-types-from-objective-c>Importing error types from Objective-C

In Objective-C, error domains are typically constructed using an enumeration describing the error codes and a constant describing the error domain, e.g,

extern NSString *const AVFoundationErrorDomain;

typedef NS_ENUM(NSInteger, AVError) {
  AVErrorUnknown = -11800,
  AVErrorOutOfMemory = -11801,
  AVErrorSessionNotRunning = -11803,
  AVErrorDeviceAlreadyUsedByAnotherSession = -11804,
  // ...
}
This is currently imported as an enum that conforms to Error:

enum AVError : Int {
  case unknown = -11800
  case outOfMemory = -11801
  case sessionNotRunning = -11803
  case deviceAlreadyUsedByAnotherSession = -11804

  static var _domain: String { return AVFoundationErrorDomain }
}
and Swift code introduces an extension that makes it an Error, along with some implementation magic to allow bridging from an NSError (losing userInfo in the process):

extension AVError : Error {
  static var _domain: String { return AVFoundationErrorDomain }
}
Instead, error enums should be expressed with a new macro, NS_ERROR_ENUM, that ties together the code and domain in the Objective-C header:

extern NSString *const AVFoundationErrorDomain;

typedef NS_ERROR_ENUM(NSInteger, AVError, AVFoundationErrorDomain) {
  AVErrorUnknown = -11800,
  AVErrorOutOfMemory = -11801,
  AVErrorSessionNotRunning = -11803,
  AVErrorDeviceAlreadyUsedByAnotherSession = -11804,
  // ...
}
This will import as a new struct AVError that contains an NSError, so there is no information loss. The actual enum will become a nested type Code, so that it is still accessible. The resulting struct will be as follows:

struct AVError {
  /// Stored NSError. Note that error.domain == AVFoundationErrorDomain is an invariant.
  private var error: NSError

  /// Describes the error codes; directly imported from AVError
  enum Code : Int, ErrorCodeProtocol {
    typealias ErrorType = AVError

    case unknown = -11800
    case outOfMemory = -11801
    case sessionNotRunning = -11803
    case deviceAlreadyUsedByAnotherSession = -11804

    func errorMatchesCode(_ error: AVError) -> Bool {
      return error.code == self
    }
  }

  /// Allow one to create an error (optionally) with a userInfo dictionary.
  init(_ code: Code, userInfo: [NSObject: AnyObject] = [:]) {
    error = NSError(code: code.rawValue, domain: _domain, userInfo: userInfo)
  }

  /// Retrieve the code.
  var code: Code { return Code(rawValue: error.code)! }

  /// Allow direct access to the userInfo dictionary.
  var userInfo: [NSObject: AnyObject] { return error.userInfo }

  /// Make it easy to refer to constants without context.
  static let unknown: Code = .unknown
  static let outOfMemory: Code = .outOfMemory
  static let sessionNotRunning: Code = .sessionNotRunning
  static let deviceAlreadyUsedByAnotherSession: Code = .deviceAlreadyUsedByAnotherSession
}

// Implementation detail: makes AVError conform to Error
extension AVError : Error {
  static var _domain: String { return AVFoundationErrorDomain }

  var _code: Int { return error.code }
}
This syntax allows one to throw specific errors fairly easily, with or without userInfo dictionaries:

throw AVError(.sessionNotRunning)
throw AVError(.sessionNotRunning, userInfo: [ ... ])
The ImportedErrorCode protocol is a helper so that we can define a general ~= operator, which is used by both switchcase matching and catch blocks:

protocol ErrorCodeProtocol {
  typealias ErrorType : Error

  func errorMatchesCode(_ error: ErrorType) -> Bool
}

func ~= <EC: ErrorCodeProtocol> (error: Error, code: EC) -> Bool {
  guard let myError = error as? EC.ErrorType else { return false }
  return code.errorMatchesCode(myError)
}
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#mapping-nserror-types-back-into-swift>Mapping NSError types back into Swift

When an NSError object bridged to an Error instance, it may be immediately mapped back to a Swift error type (e.g., if the error was created as a HomeworkError instance in Swift and then passed through NSError unmodified) or it might be leave as an instance of NSError. The error might then be catch as a particular Swift error type, e.g.,

catch let error as AVError where error.code == .sessionNotRunning {
  // able to access userInfo here!
}
In this case, the mapping from an NSError instance to AVError goes through an implementation-detail protocol_ObjectiveCBridgeableError:

protocol _ObjectiveCBridgeableError : Error {
  /// Produce a value of the error type corresponding to the given NSError,
  /// or return nil if it cannot be bridged.
  init?(_bridgedNSError error: NSError)
}
The initializer is responsible for checking the domain and (optionally) the code of the incoming NSError to map it to an instance of the Swift error type. For example, AVError would adopt this protocol as follows:

// Implementation detail: makes AVError conform to _ObjectiveCBridgeableError
extension AVError : _ObjectiveCBridgeableError {
  init?(_bridgedNSError error: NSError) {
    // Check whether the error comes from the AVFoundation error domain
    if error.domain != AVFoundationErrorDomain { return nil }

    // Save the error
    self.error = error
  }
}
We do not propose that _ObjectiveCBridgeableError become a public protocol, because the core team has already deferred a similar proposal (SE-0058 <https://github.com/apple/swift-evolution/blob/master/proposals/0058-objectivecbridgeable.md>) to make the related protocol _ObjectiveCBridgeable public.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#other-issues>Other Issues

NSError codes and domains are important for localization of error messages. This is barely supported today by genstrings, but becomes considerably harder when the domain and code are hidden (as they are in Swift). We would need to consider tooling to make it easier to localize error descriptions, recovery options, etc. in a sensible way. Although this is out of the scope of the Swift language per se, it's an important part of the developer story.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#impact-on-existing-code>Impact on existing code

This is a major source-breaking change for Objective-C APIs that operate on NSError values, because those parameter/return/property types will change from NSError to Error. There are ~400 such APIs in the macOS SDK, and closer to 500 in the iOS SDK, which is a sizable number. Fortunately, this is similar in scope to the Foundation value types proposal <https://github.com/apple/swift-evolution/blob/master/proposals/0069-swift-mutability-for-foundation.md>, and can use the same code migration mechanism. That said, the scale of this change means that it should either happen in Swift 3 or not at all.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#future-directions>Future directions

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#better-tooling-for-describing-errors>Better tooling for describing errors

When adopting one of the new protocols (e.g., LocalizedError) in an enum, one will inevitably end up with a number of switch statements that have to enumerate all of the cases, leading to a lot of boilerplate. Better tooling could improve the situation considerably: for example, one could use something like Cocoa's stringsdict files <https://developer.apple.com/library/prerelease/content/documentation/MacOSX/Conceptual/BPInternational/StringsdictFileFormat/StringsdictFileFormat.html> to provide localized strings identified by the enum name, case name, and property. That would eliminate the need for the switch-on-all-cases implementations of each property.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#round-tripping-errors-through-userinfo>Round-tripping errors through userInfo

The CustomNSError protocol allows one to place arbitrary key/value pairs into NSError's userInfo dictionary. The implementation-detail _ObjectiveCBridgeableError protocol allows one to control how a raw NSError is mapped to a particular error type. One could effectively serialize the entire state of a particular error type into the userInfo dictionary viaCustomNSError, then restore it via _ObjectiveCBridgeableError, allowing one to form a complete NSError in Objective-C that can reconstitute itself as a particular Swift error type, which can be useful both for mixed-source projects and (possibly) as a weak form of serialization for NSErrors.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#alternatives-considered>Alternatives considered

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#exposing-the-domain-code-and-user-info-dictionary-directly>Exposing the domain, code, and user-info dictionary directly

This proposal does not directly expose the domain, code, or user-info dictionary on ErrorProtocol, because these notions are superseded by Swift's strong typing of errors. The domain is effectively subsumed by the type of the error (e.g., a Swift-defined error type uses its mangled name as the domain); the code is some type-specific value (e.g., the discriminator of the enum); and the user-info dictionary is an untyped set of key-value pairs that are better expressed in Swift as data on the specific error type.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#bridging-nserror-to-a-new-value-type-error>Bridging NSError to a new value type Error

One could introduce a new value type, Error, that stores a domain, code, and user-info dictionary but provides them with value semantics. Doing so would make it easier to create "generic" errors that carry some information. However, we feel that introducing new error types in Swift is already easier than establishing a new domain and a set of codes, because a new enum type provides this information naturally in Swift.


(Charles Srstka) #2

Obviously, I’m in favor of this one. +1!

I think I did prefer the older name of CustomUserInfoError for the domain/code/userInfo protocol, rather than CustomNSError. This is just because I’d like to be able to do a global search through my project for “NSError” and have it turn up empty. Maybe a silly reason, I know. :wink:

I sent a few more comments off-list before I realized you’d posted the proposal already, so you can peruse, consider, and/or discard those at your leisure. It’s nothing terribly important, though; this proposal is pretty great as is.

Thanks so much for doing this! I’m really excited for better error handling in Swift 3.

Charles

···

On Jun 27, 2016, at 1:17 PM, Douglas Gregor <dgregor@apple.com> wrote:

Hi all,

Proposal link: https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md

Here is a detailed proposal draft for bridging NSError to ErrorProtocol. Getting this right is surprisingly involved, so the detailed design on this proposal is fairly large. Comments welcome!

  - Doug

NSError Bridging

Proposal: SE-NNNN <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/NNNN-nserror-bridging.md>
Author: Doug Gregor <https://github.com/DougGregor>, Charles Srstka <https://github.com/CharlesJS>
Status: Awaiting review
Review manager: TBD
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#introduction>Introduction

Swift's error handling model interoperates directly with Cocoa's NSError conventions. For example, an Objective-C method with an NSError** parameter, e.g.,

- (nullable NSURL *)replaceItemAtURL:(NSURL *)url options:(NSFileVersionReplacingOptions)options error:(NSError **)error;
will be imported as a throwing method:

func replaceItem(at url: URL, options: ReplacingOptions = []) throws -> URL
Swift bridges between ErrorProtocol-conforming types and NSError so, for example, a Swift enum that conforms toErrorProtocol can be thrown and will be reflected as an NSError with a suitable domain and code. Moreover, an NSErrorproduced with that domain and code can be caught as the Swift enum type, providing round-tripping so that Swift can deal in ErrorProtocol values while Objective-C deals in NSError objects.

However, the interoperability is incomplete in a number of ways, which results in Swift programs having to walk a careful line between the ErrorProtocol-based Swift way and the NSError-based way. This proposal attempts to bridge those gaps.

Swift-evolution thread: Charles Srstka's pitch for Consistent bridging for NSErrors at the language boundary <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160502/016618.html>, which discussed Charles' original proposal <https://github.com/apple/swift-evolution/pull/331> that addressed these issues by providing NSError to ErrorProtocol bridging and exposing the domain, code, and user-info dictionary for all errors. This proposal expands upon that work, but without directly exposing the domain, code, and user-info.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#motivation>Motivation

There are a number of weaknesses in Swift's interoperability with Cocoa's error model, including:

There is no good way to provide important error information when throwing an error from Swift. For example, let's consider a simple application-defined error in Swift:

enum HomeworkError : Int, ErrorProtocol {
  case forgotten
  case lost
  case dogAteIt
}
One can throw HomeworkError.dogAteIt and it can be interpreted as an NSError by Objective-C with an appropriate error domain (effectively, the mangled name of the HomeworkError type) and code (effectively, the case discriminator). However, one cannot provide a localized description, help anchor, recovery attempter, or any other information commonly placed into the userInfo dictionary of an NSError. To provide these values, one must specifically construct an NSError in Swift, e.g.,

throw NSError(code: HomeworkError.dogAteIt.rawValue,
              domain: HomeworkError._domain,
              userInfo: [ NSLocalizedDescriptionKey : "the dog ate it" ])
There is no good way to get information typically associated with NSError's userInfo in Swift. For example, the Swift-natural way to catch a specific error in the AVError error domain doesn't give one access to the userInfo dictionary, e.g.,:

catch let error as AVError where error == .diskFull {
  // AVError is an enum, so one only gets the equivalent of the code.
  // There is no way to access the localized description (for example) or
  // any other information typically stored in the ``userInfo`` dictionary.
}
The workaround is to catch as an NSError, which is quite a bit more ugly:

catch let error as NSError where error._domain == AVFoundationErrorDomain && error._code == AVFoundationErrorDomain.diskFull.rawValue {
  // okay: userInfo is finally accessible, but still weakly typed
}
This makes it extremely hard to access common information, such as the localized description. Moreover, the userInfo dictionary is effectively untyped so, for example, one has to know a priori that the value associated with the known AVErrorDeviceKey will be typed as CMTime:

catch let error as NSError where error._domain = AVFoundationErrorDomain {
  if let time = error.userInfo[AVErrorDeviceKey] as? CMTime {
    // ...
  }
}
It would be far better if one could catch an AVError directly and query the time in a type-safe manner:

catch let error as AVError {
  if let time = error.time {
    // ...
  }
}
NSError is inconsistently bridged with ErrorProtocol. Swift interoperates by translating between NSError and ErrorProtocol when mapping between a throwing Swift method/initializer and an Objective-C method with an NSError** parameter. However, an Objective-C method that takes an NSError* parameter (e.g., to render it) does not bridge to ErrorProtocol, meaning that NSError is part of the API in Swift in some places (but not others). For example, NSError leaks through when the following UIDocument API in Objective-C:

- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
is imported into Swift as follows:

func handleError(_ error: NSError, userInteractionPermitted: Bool)
One would expect the first parameter to be imported as ErrorProtocol.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#proposed-solution>Proposed solution

This proposal involves directly addressing (1)-(3) with new protocols and a different way of bridging Objective-C error code types into Swift, along with some conveniences for working with Cocoa errors:

Introduce three new protocols for describing more information about errors: LocalizedError, RecoverableError, andCustomNSError. For example, an error type can provide a localized description by conforming to LocalizedError:

extension HomeworkError : LocalizedError {
  var errorDescription: String? {
    switch self {
    case .forgotten: return NSLocalizedString("I forgot it")
    case .lost: return NSLocalizedString("I lost it")
    case .dogAteIt: return NSLocalizedString("The dog ate it")
    }
  }
}
Imported Objective-C error types should be mapped to struct types that store an NSError so that no information is lost when bridging from an NSError to the Swift error types. We propose to introduce a new macro, NS_ERROR_ENUM, that one can use to both declare an enumeration type used to describe the error codes as well as tying that type to a specific domain constant, e.g.,

typedef NS_ERROR_ENUM(NSInteger, AVError, AVFoundationErrorDomain) {
  AVErrorUnknown = -11800,
  AVErrorOutOfMemory = -11801,
  AVErrorSessionNotRunning = -11803,
  AVErrorDeviceAlreadyUsedByAnotherSession = -11804,
  // ...
}
The imported AVError will have a struct that allows one to access the userInfo dictionary directly. This retains the ability to catch via a specific code, e.g.,

catch AVError.outOfMemory {
// ...
}
However, catching a specific error as a value doesn't lose information:

catch let error as AVError where error.code == .sessionNotRunning {
// able to access userInfo here!
}
This also gives the ability for one to add typed accessors for known keys within the userInfo dictionary:

extension AVError {
var time: CMTime? {
   get {
     return userInfo[AVErrorTimeKey] as? CMTime?
   }

   set {
     userInfo[AVErrorTimeKey] = newValue.map { $0 as CMTime }
   }
}
}
Bridge NSError to ErrorProtocol, so that all NSError uses are bridged consistently. For example, this means that the Objective-C API:

- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
is imported into Swift as:

func handleError(_ error: ErrorProtocol, userInteractionPermitted: Bool)
This will use the same bridging logic in the Clang importer that we use for other value types (Array, String, URL, etc.), but with the runtime translation we've already been doing for catching/throwing errors.

When we introduce this bridging, we will need to remove NSError's conformance to ErrorProtocol to avoid creating cyclic implicit conversions. However, we still need an easy way to create an ErrorProtocol instance from an arbitrary NSError, e.g.,

extension NSError {
  var asError: ErrorProtocol { ... }
}
In Foundation, add an extension to ErrorProtocol that provides typed access to the common user-info keys. Note that we focus only on those user-info keys that are read by user code (vs. only accessed by frameworks):

extension ErrorProtocol {
  // Note: for exposition only. Not actual API.
  private var userInfo: [NSObject : AnyObject] {
    return (self as! NSError).userInfo
  }

  var localizedDescription: String {
    return (self as! NSError).localizedDescription
  }

  var filePath: String? {
    return userInfo[NSFilePathErrorKey] as? String
  }

  var stringEncoding: String.Encoding? {
    return (userInfo[NSStringEncodingErrorKey] as? NSNumber)
             .map { String.Encoding(rawValue: $0.uintValue) }
  }

  var underlying: ErrorProtocol? {
    return (userInfo[NSUnderlyingErrorKey] as? NSError)?.asError
  }

  var url: URL? {
    return userInfo[NSURLErrorKey] as? URL
  }
}
Rename ErrorProtocol to Error: once we've completed the bridging story, Error becomes the primary way to work with error types in Swift, and the value type to which NSError is bridged:

func handleError(_ error: Error, userInteractionPermitted: Bool)
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#detailed-design>Detailed design

This section details both the design (including the various new protocols, mapping from Objective-C error code enumeration types into Swift types, etc.) and the efficient implementation of this design to interoperate with NSError. Throughout the detailed design, we already assume the name change from ErrorProtocol to Error.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#new-protocols>New protocols

This proposal introduces several new protocols that allow error types to expose more information about error types.

The LocalizedError protocol describes an error that provides localized messages for display to the end user, all of which provide default implementations. The conforming type can provide implementations for any subset of these requirements:

protocol LocalizedError : Error {
  /// A localized message describing what error occurred.
  var errorDescription: String? { get }

  /// A localized message describing the reason for the failure.
  var failureReason: String? { get }

  /// A localized message describing how one might recover from the failure.
  var recoverySuggestion: String? { get }

  /// A localized message providing "help" text if the user requests help.
  var helpAnchor: String? { get }
}

extension LocalizedError {
  var errorDescription: String? { return nil }
  var failureReason: String? { return nil }
  var recoverySuggestion: String? { return nil }
  var helpAnchor: String? { return nil }
}
The RecoverableError protocol describes an error that might be recoverable:

protocol RecoverableError : Error {
  /// Provides a set of possible recovery options to present to the user.
  var recoveryOptions: [String] { get }

  /// Attempt to recover from this error when the user selected the
  /// option at the given index. This routine must call resultHandler and
  /// indicate whether recovery was successful (or not).
  ///
  /// This entry point is used for recovery of errors handled at a
  /// "document" granularity, that do not affect the entire
  /// application.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int,
                       andThen resultHandler: (recovered: Bool) -> Void)

  /// Attempt to recover from this error when the user selected the
  /// option at the given index. Returns true to indicate
  /// successful recovery, and false otherwise.
  ///
  /// This entry point is used for recovery of errors handled at
  /// the "application" granularity, where nothing else in the
  /// application can proceed until the attmpted error recovery
  /// completes.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool
}

extension RecoverableError {
  /// By default, implements document-modal recovery via application-model
  /// recovery.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int,
                       andThen resultHandler: (Bool) -> Void) {
    resultHandler(recovered: attemptRecovery(optionIndex: recoveryOptionIndex))
  }
}
Error types that conform to RecoverableError may be given an opportunity to recover from the error. The user can be presented with some number of (localized) recovery options, described by recoveryOptions, and the selected option will be passed to the appropriate attemptRecovery method.

The CustomNSError protocol describes an error that wants to provide custom NSError information. This can be used, e.g., to provide a specific domain/code or to populate NSError's userInfo dictionary with values for custom keys that can be accessed from Objective-C code but are not covered by the other protocols.

/// Describes an error type that fills in the userInfo directly.
protocol CustomNSError : Error {
  var errorDomain: String { get }
  var errorCode: Int { get }
  var errorUserInfo: [String : AnyObject] { get }
}
Note that, unlike with NSError, the provided errorUserInfo requires String keys. This is in line with common practice for NSError and is important for the implementation (see below). All of these properties are defaulted, so one can provide any subset:

extension CustomNSError {
  var errorDomain: String { ... }
  var errorCode: Int { ... }
  var errorUserInfo: [String : AnyObject] { ... }
}
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#mapping-error-types-to-nserror>Mapping error types to NSError

Every type that conforms to the Error protocol is implicitly bridged to NSError. This has been the case since Swift 2, where the compiler provides a domain (i.e., the mangled name of the type) and code (based on the discriminator of the enumeration type). This proposal also allows for the userInfo dictionary to be populated by the runtime, which will check for conformance to the various protocols (LocalizedError, RecoverableError, or CustomNSError) to retrieve information.

Conceptually, this could be implemented by eagerly creating a userInfo dictionary for a given instance of Error:

func createUserInfo(error: Error) -> [NSObject : AnyObject] {
  var userInfo: [NSObject : AnyObject] = [:]

  // Retrieve custom userInfo information.
  if let customUserInfoError = error as? CustomNSError {
    userInfo = customUserInfoError.userInfo
  }

  if let localizedError = error as? LocalizedError {
    if let description = localizedError.errorDescription {
      userInfo[NSLocalizedDescriptionKey] = description
    }

    if let reason = localizedError.failureReason {
      userInfo[NSLocalizedFailureReasonErrorKey] = reason
    }

    if let suggestion = localizedError.recoverySuggestion {
      userInfo[NSLocalizedRecoverySuggestionErrorKey] = suggestion
    }

    if let helpAnchor = localizedError.helpAnchor {
      userInfo[NSHelpAnchorErrorKey] = helpAnchor
    }
  }

  if let recoverableError = error as? RecoverableError {
    userInfo[NSLocalizedRecoveryOptionsErrorKey] = recoverableError.recoveryOptions
    userInfo[NSRecoveryAttempterErrorKey] = RecoveryAttempter()
  }
}
The RecoveryAttempter class is an implementation detail. It will implement the informal protocol NSErrorRecoveryAttempting <https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Protocols/NSErrorRecoveryAttempting_Protocol/> for the given error:

class RecoveryAttempter : NSObject {
  @objc(attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo:)
  func attemptRecovery(fromError nsError: NSError,
                       optionIndex recoveryOptionIndex: Int,
                       delegate: AnyObject?,
                       didRecoverSelector: Selector,
                       contextInfo: UnsafeMutablePointer<Void>) {
    let error = nsError as! RecoverableError
    error.attemptRecovery(optionIndex: recoveryOptionIndex) { success in
      // Exposition only: this part will actually have to be
      // implemented in Objective-C to pass the BOOL and void* through.
      delegate?.perform(didRecoverSelector, with: success, with: contextInfo)
    }
  }

  @objc(attemptRecoveryFromError:optionIndex:)
  func attemptRecovery(fromError nsError: NSError,
                       optionIndex recoveryOptionIndex: Int) -> Bool {
    let error = nsError as! RecoverableError
    return error.attemptRecovery(optionIndex: recoveryOptionIndex)
  }
}
The actual the population of the userInfo dictionary should not be eager. NSError provides the notion of global "user info value providers" that it uses to lazily request the values for certain keys, via setUserInfoValueProvider(forDomain:provider:), which is declared as:

extension NSError {
  @available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *)
  class func setUserInfoValueProvider(forDomain errorDomain: String,
                                      provider: ((NSError, String) -> AnyObject?)? = nil)
}
The runtime would need to register a user info value provider for each error type the first time it is bridged into NSError, supplying the domain and the following user info value provider function:

func userInfoValueProvider(nsError: NSError, key: String) -> AnyObject? {
  let error = nsError as! Error
  switch key {
  case NSLocalizedDescriptionKey:
    return (error as? LocalizedError)?.errorDescription

  case NSLocalizedFailureReasonErrorKey:
    return (error as? LocalizedError)?.failureReason

  case NSLocalizedRecoverySuggestionErrorKey:
    return (error as? LocalizedError)?.recoverySuggestion

  case NSHelpAnchorErrorKey:
    return (error as? LocalizedError)?.helpAnchor

  case NSLocalizedRecoveryOptionsErrorKey:
    return (error as? RecoverableError)?.recoveryOptions

  case NSRecoveryAttempterErrorKey:
    return error is RecoverableError ? RecoveryAttempter() : nil

  default:
    guard let customUserInfoError = error as? CustomNSError else { return nil }
    return customUserInfoError.userInfo[key]
  }
}
On platforms that predate the introduction of user info value providers, there are alternate implementation strategies, including introducing a custom NSDictionary subclass to use as the userInfo in the NSError that lazily populates the dictionary by, effectively, calling the userInfoValueProvider function above for each requested key. Or, one could eagerly populate userInfo on older platforms.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#importing-error-types-from-objective-c>Importing error types from Objective-C

In Objective-C, error domains are typically constructed using an enumeration describing the error codes and a constant describing the error domain, e.g,

extern NSString *const AVFoundationErrorDomain;

typedef NS_ENUM(NSInteger, AVError) {
  AVErrorUnknown = -11800,
  AVErrorOutOfMemory = -11801,
  AVErrorSessionNotRunning = -11803,
  AVErrorDeviceAlreadyUsedByAnotherSession = -11804,
  // ...
}
This is currently imported as an enum that conforms to Error:

enum AVError : Int {
  case unknown = -11800
  case outOfMemory = -11801
  case sessionNotRunning = -11803
  case deviceAlreadyUsedByAnotherSession = -11804

  static var _domain: String { return AVFoundationErrorDomain }
}
and Swift code introduces an extension that makes it an Error, along with some implementation magic to allow bridging from an NSError (losing userInfo in the process):

extension AVError : Error {
  static var _domain: String { return AVFoundationErrorDomain }
}
Instead, error enums should be expressed with a new macro, NS_ERROR_ENUM, that ties together the code and domain in the Objective-C header:

extern NSString *const AVFoundationErrorDomain;

typedef NS_ERROR_ENUM(NSInteger, AVError, AVFoundationErrorDomain) {
  AVErrorUnknown = -11800,
  AVErrorOutOfMemory = -11801,
  AVErrorSessionNotRunning = -11803,
  AVErrorDeviceAlreadyUsedByAnotherSession = -11804,
  // ...
}
This will import as a new struct AVError that contains an NSError, so there is no information loss. The actual enum will become a nested type Code, so that it is still accessible. The resulting struct will be as follows:

struct AVError {
  /// Stored NSError. Note that error.domain == AVFoundationErrorDomain is an invariant.
  private var error: NSError

  /// Describes the error codes; directly imported from AVError
  enum Code : Int, ErrorCodeProtocol {
    typealias ErrorType = AVError

    case unknown = -11800
    case outOfMemory = -11801
    case sessionNotRunning = -11803
    case deviceAlreadyUsedByAnotherSession = -11804

    func errorMatchesCode(_ error: AVError) -> Bool {
      return error.code == self
    }
  }

  /// Allow one to create an error (optionally) with a userInfo dictionary.
  init(_ code: Code, userInfo: [NSObject: AnyObject] = [:]) {
    error = NSError(code: code.rawValue, domain: _domain, userInfo: userInfo)
  }

  /// Retrieve the code.
  var code: Code { return Code(rawValue: error.code)! }

  /// Allow direct access to the userInfo dictionary.
  var userInfo: [NSObject: AnyObject] { return error.userInfo }

  /// Make it easy to refer to constants without context.
  static let unknown: Code = .unknown
  static let outOfMemory: Code = .outOfMemory
  static let sessionNotRunning: Code = .sessionNotRunning
  static let deviceAlreadyUsedByAnotherSession: Code = .deviceAlreadyUsedByAnotherSession
}

// Implementation detail: makes AVError conform to Error
extension AVError : Error {
  static var _domain: String { return AVFoundationErrorDomain }

  var _code: Int { return error.code }
}
This syntax allows one to throw specific errors fairly easily, with or without userInfo dictionaries:

throw AVError(.sessionNotRunning)
throw AVError(.sessionNotRunning, userInfo: [ ... ])
The ImportedErrorCode protocol is a helper so that we can define a general ~= operator, which is used by both switchcase matching and catch blocks:

protocol ErrorCodeProtocol {
  typealias ErrorType : Error

  func errorMatchesCode(_ error: ErrorType) -> Bool
}

func ~= <EC: ErrorCodeProtocol> (error: Error, code: EC) -> Bool {
  guard let myError = error as? EC.ErrorType else { return false }
  return code.errorMatchesCode(myError)
}
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#mapping-nserror-types-back-into-swift>Mapping NSError types back into Swift

When an NSError object bridged to an Error instance, it may be immediately mapped back to a Swift error type (e.g., if the error was created as a HomeworkError instance in Swift and then passed through NSError unmodified) or it might be leave as an instance of NSError. The error might then be catch as a particular Swift error type, e.g.,

catch let error as AVError where error.code == .sessionNotRunning {
  // able to access userInfo here!
}
In this case, the mapping from an NSError instance to AVError goes through an implementation-detail protocol_ObjectiveCBridgeableError:

protocol _ObjectiveCBridgeableError : Error {
  /// Produce a value of the error type corresponding to the given NSError,
  /// or return nil if it cannot be bridged.
  init?(_bridgedNSError error: NSError)
}
The initializer is responsible for checking the domain and (optionally) the code of the incoming NSError to map it to an instance of the Swift error type. For example, AVError would adopt this protocol as follows:

// Implementation detail: makes AVError conform to _ObjectiveCBridgeableError
extension AVError : _ObjectiveCBridgeableError {
  init?(_bridgedNSError error: NSError) {
    // Check whether the error comes from the AVFoundation error domain
    if error.domain != AVFoundationErrorDomain { return nil }

    // Save the error
    self.error = error
  }
}
We do not propose that _ObjectiveCBridgeableError become a public protocol, because the core team has already deferred a similar proposal (SE-0058 <https://github.com/apple/swift-evolution/blob/master/proposals/0058-objectivecbridgeable.md>) to make the related protocol _ObjectiveCBridgeable public.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#other-issues>Other Issues

NSError codes and domains are important for localization of error messages. This is barely supported today by genstrings, but becomes considerably harder when the domain and code are hidden (as they are in Swift). We would need to consider tooling to make it easier to localize error descriptions, recovery options, etc. in a sensible way. Although this is out of the scope of the Swift language per se, it's an important part of the developer story.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#impact-on-existing-code>Impact on existing code

This is a major source-breaking change for Objective-C APIs that operate on NSError values, because those parameter/return/property types will change from NSError to Error. There are ~400 such APIs in the macOS SDK, and closer to 500 in the iOS SDK, which is a sizable number. Fortunately, this is similar in scope to the Foundation value types proposal <https://github.com/apple/swift-evolution/blob/master/proposals/0069-swift-mutability-for-foundation.md>, and can use the same code migration mechanism. That said, the scale of this change means that it should either happen in Swift 3 or not at all.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#future-directions>Future directions

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#better-tooling-for-describing-errors>Better tooling for describing errors

When adopting one of the new protocols (e.g., LocalizedError) in an enum, one will inevitably end up with a number of switch statements that have to enumerate all of the cases, leading to a lot of boilerplate. Better tooling could improve the situation considerably: for example, one could use something like Cocoa's stringsdict files <https://developer.apple.com/library/prerelease/content/documentation/MacOSX/Conceptual/BPInternational/StringsdictFileFormat/StringsdictFileFormat.html> to provide localized strings identified by the enum name, case name, and property. That would eliminate the need for the switch-on-all-cases implementations of each property.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#round-tripping-errors-through-userinfo>Round-tripping errors through userInfo

The CustomNSError protocol allows one to place arbitrary key/value pairs into NSError's userInfo dictionary. The implementation-detail _ObjectiveCBridgeableError protocol allows one to control how a raw NSError is mapped to a particular error type. One could effectively serialize the entire state of a particular error type into the userInfo dictionary viaCustomNSError, then restore it via _ObjectiveCBridgeableError, allowing one to form a complete NSError in Objective-C that can reconstitute itself as a particular Swift error type, which can be useful both for mixed-source projects and (possibly) as a weak form of serialization for NSErrors.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#alternatives-considered>Alternatives considered

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#exposing-the-domain-code-and-user-info-dictionary-directly>Exposing the domain, code, and user-info dictionary directly

This proposal does not directly expose the domain, code, or user-info dictionary on ErrorProtocol, because these notions are superseded by Swift's strong typing of errors. The domain is effectively subsumed by the type of the error (e.g., a Swift-defined error type uses its mangled name as the domain); the code is some type-specific value (e.g., the discriminator of the enum); and the user-info dictionary is an untyped set of key-value pairs that are better expressed in Swift as data on the specific error type.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#bridging-nserror-to-a-new-value-type-error>Bridging NSError to a new value type Error

One could introduce a new value type, Error, that stores a domain, code, and user-info dictionary but provides them with value semantics. Doing so would make it easier to create "generic" errors that carry some information. However, we feel that introducing new error types in Swift is already easier than establishing a new domain and a set of codes, because a new enum type provides this information naturally in Swift.


(Charles Srstka) #3

Obviously, I’m in favor of this one. +1!

I think I did prefer the older name of CustomUserInfoError for the domain/code/userInfo protocol, rather than CustomNSError. This is just because I’d like to be able to do a global search through my project for “NSError” and have it turn up empty. Maybe a silly reason, I know. :wink:

I sent a few more comments off-list before I realized you’d posted the proposal already, so you can peruse, consider, and/or discard those at your leisure. It’s nothing terribly important, though; this proposal is pretty great as is.

Thanks so much for doing this! I’m really excited for better error handling in Swift 3.

Charles

···

On Jun 27, 2016, at 1:17 PM, Douglas Gregor <dgregor@apple.com> wrote:

Hi all,

Proposal link: https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md

Here is a detailed proposal draft for bridging NSError to ErrorProtocol. Getting this right is surprisingly involved, so the detailed design on this proposal is fairly large. Comments welcome!

  - Doug

NSError Bridging

  • Proposal: SE-NNNN
  • Author: Doug Gregor, Charles Srstka
  • Status: Awaiting review
  • Review manager: TBD
Introduction

Swift's error handling model interoperates directly with Cocoa's NSError conventions. For example, an Objective-C method with an NSError** parameter, e.g.,

- (nullable NSURL *)replaceItemAtURL:(NSURL *)url options:(NSFileVersionReplacingOptions)options error:(NSError **)error;
will be imported as a throwing method:

func replaceItem(at url: URL, options: ReplacingOptions = []) throws -> URL
Swift bridges between ErrorProtocol-conforming types and NSError so, for example, a Swift enum that conforms toErrorProtocol can be thrown and will be reflected as an NSError with a suitable domain and code. Moreover, an NSErrorproduced with that domain and code can be caught as the Swift enum type, providing round-tripping so that Swift can deal in ErrorProtocol values while Objective-C deals in NSError objects.

However, the interoperability is incomplete in a number of ways, which results in Swift programs having to walk a careful line between the ErrorProtocol-based Swift way and the NSError-based way. This proposal attempts to bridge those gaps.

Swift-evolution thread: Charles Srstka's pitch for Consistent bridging for NSErrors at the language boundary, which discussed Charles' original proposal that addressed these issues by providing NSError to ErrorProtocol bridging and exposing the domain, code, and user-info dictionary for all errors. This proposal expands upon that work, but without directly exposing the domain, code, and user-info.

Motivation

There are a number of weaknesses in Swift's interoperability with Cocoa's error model, including:

  • There is no good way to provide important error information when throwing an error from Swift. For example, let's consider a simple application-defined error in Swift:

enum HomeworkError : Int
, ErrorProtocol {
  
case
forgotten
  
case
lost
  
case
dogAteIt
}

One can throw HomeworkError.dogAteIt and it can be interpreted as an NSError by Objective-C with an appropriate error domain (effectively, the mangled name of the HomeworkError type) and code (effectively, the case discriminator). However, one cannot provide a localized description, help anchor, recovery attempter, or any other information commonly placed into the userInfo dictionary of an NSError. To provide these values, one must specifically construct an NSError in Swift, e.g.,

throw NSError(code: HomeworkError.dogAteIt.rawValue
,
              domain: HomeworkError
.
_domain,
              userInfo: [ NSLocalizedDescriptionKey
: "the dog ate it" ])
  • There is no good way to get information typically associated with NSError's userInfo in Swift. For example, the Swift-natural way to catch a specific error in the AVError error domain doesn't give one access to the userInfo dictionary, e.g.,:

catch let error as AVError where error == .
diskFull {
  
// AVError is an enum, so one only gets the equivalent of the code.

// There is no way to access the localized description (for example) or

// any other information typically stored in the ``userInfo`` dictionary.

}

The workaround is to catch as an NSError, which is quite a bit more ugly:

catch let error as NSError where error._domain == AVFoundationErrorDomain && error._code == AVFoundationErrorDomain.diskFull.rawValue
{
  
// okay: userInfo is finally accessible, but still weakly typed

}

This makes it extremely hard to access common information, such as the localized description. Moreover, the userInfo dictionary is effectively untyped so, for example, one has to know a priori that the value associated with the known AVErrorDeviceKey will be typed as CMTime:

catch let error as NSError where error._domain =
AVFoundationErrorDomain {
  
if let time = error.userInfo[AVErrorDeviceKey] as?
CMTime {
    
// ...

  }
}

It would be far better if one could catch an AVError directly and query the time in a type-safe manner:

catch let error as
AVError {
  
if let time = error.
time {
    
// ...

  }
}

  • NSError is inconsistently bridged with ErrorProtocol. Swift interoperates by translating between NSError and ErrorProtocol when mapping between a throwing Swift method/initializer and an Objective-C method with an NSError** parameter. However, an Objective-C method that takes an NSError* parameter (e.g., to render it) does not bridge to ErrorProtocol, meaning that NSError is part of the API in Swift in some places (but not others). For example, NSError leaks through when the following UIDocument API in Objective-C:

- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
is imported into Swift as follows:

func handleError(_ error: NSError, userInteractionPermitted: Bool)
One would expect the first parameter to be imported as ErrorProtocol.

Proposed solution

This proposal involves directly addressing (1)-(3) with new protocols and a different way of bridging Objective-C error code types into Swift, along with some conveniences for working with Cocoa errors:

  • Introduce three new protocols for describing more information about errors: LocalizedError, RecoverableError, andCustomNSError. For example, an error type can provide a localized description by conforming to LocalizedError:

extension HomeworkError :
LocalizedError {
  
var errorDescription: String
? {
    
switch self
{
    
case .forgotten: return NSLocalizedString("I forgot it"
)
    
case .lost: return NSLocalizedString("I lost it"
)
    
case .dogAteIt: return NSLocalizedString("The dog ate it"
)
    }
  }
}

  • Imported Objective-C error types should be mapped to struct types that store an NSError so that no information is lost when bridging from an NSError to the Swift error types. We propose to introduce a new macro, NS_ERROR_ENUM, that one can use to both declare an enumeration type used to describe the error codes as well as tying that type to a specific domain constant, e.g.,

typedef NS_ERROR_ENUM(NSInteger
, AVError, AVFoundationErrorDomain) {
  AVErrorUnknown = -
11800
,
  AVErrorOutOfMemory = -
11801
,
  AVErrorSessionNotRunning = -
11803
,
  AVErrorDeviceAlreadyUsedByAnotherSession = -
11804
,
  
// ...

}

The imported AVError will have a struct that allows one to access the userInfo dictionary directly. This retains the ability to catch via a specific code, e.g.,

catch AVError.
outOfMemory {

// ...

}

However, catching a specific error as a value doesn't lose information:

catch let error as AVError where error.code == .
sessionNotRunning {

// able to access userInfo here!

}

This also gives the ability for one to add typed accessors for known keys within the userInfo dictionary:

extension
AVError {

var
time: CMTime? {
   
get
{
     
return userInfo[AVErrorTimeKey] as?
CMTime?
   }

set
{
     userInfo[AVErrorTimeKey]
= newValue.map { $0 as
CMTime }
   }
}
}

  • Bridge NSError to ErrorProtocol, so that all NSError uses are bridged consistently. For example, this means that the Objective-C API:

- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
is imported into Swift as:

func handleError(_ error: ErrorProtocol, userInteractionPermitted: Bool)
This will use the same bridging logic in the Clang importer that we use for other value types (Array, String, URL, etc.), but with the runtime translation we've already been doing for catching/throwing errors.

When we introduce this bridging, we will need to remove NSError's conformance to ErrorProtocol to avoid creating cyclic implicit conversions. However, we still need an easy way to create an ErrorProtocol instance from an arbitrary NSError, e.g.,

extension
NSError {
  
var asError: ErrorProtocol { ...
}
}

  • In Foundation, add an extension to ErrorProtocol that provides typed access to the common user-info keys. Note that we focus only on those user-info keys that are read by user code (vs. only accessed by frameworks):

extension
ErrorProtocol {
  
// Note: for exposition only. Not actual API.

private var userInfo: [NSObject : AnyObject
] {
    
return (self as! NSError).
userInfo
  }

var localizedDescription: String
{
    
return (self as! NSError).
localizedDescription
  }

var filePath: String
? {
    
return userInfo[NSFilePathErrorKey] as? String

  }

var stringEncoding: String.
Encoding? {
    
return (userInfo[NSStringEncodingErrorKey] as?
NSNumber)
             
.map { String.Encoding(rawValue: $0.uintValue
) }
  }

var
underlying: ErrorProtocol? {
    
return (userInfo[NSUnderlyingErrorKey] as? NSError)?.
asError
  }

var
url: URL? {
    
return userInfo[NSURLErrorKey] as?
URL
  }
}

  • Rename ErrorProtocol to Error: once we've completed the bridging story, Error becomes the primary way to work with error types in Swift, and the value type to which NSError is bridged:

func handleError(_ error: Error, userInteractionPermitted: Bool)
Detailed design

This section details both the design (including the various new protocols, mapping from Objective-C error code enumeration types into Swift types, etc.) and the efficient implementation of this design to interoperate with NSError. Throughout the detailed design, we already assume the name change from ErrorProtocol to Error.

New protocols

This proposal introduces several new protocols that allow error types to expose more information about error types.

The LocalizedError protocol describes an error that provides localized messages for display to the end user, all of which provide default implementations. The conforming type can provide implementations for any subset of these requirements:

protocol LocalizedError :
Error {
  
/// A localized message describing what error occurred.

var errorDescription: String? { get
}

/// A localized message describing the reason for the failure.

var failureReason: String? { get
}

/// A localized message describing how one might recover from the failure.

var recoverySuggestion: String? { get
}

/// A localized message providing "help" text if the user requests help.

var helpAnchor: String? { get
}
}

extension
LocalizedError {
  
var errorDescription: String? { return nil
}
  
var failureReason: String? { return nil
}
  
var recoverySuggestion: String? { return nil
}
  
var helpAnchor: String? { return nil
}
}

The RecoverableError protocol describes an error that might be recoverable:

protocol RecoverableError :
Error {
  
/// Provides a set of possible recovery options to present to the user.

var recoveryOptions: [String] { get
}

/// Attempt to recover from this error when the user selected the

/// option at the given index. This routine must call resultHandler and

/// indicate whether recovery was successful (or not).

///

/// This entry point is used for recovery of errors handled at a

/// "document" granularity, that do not affect the entire

/// application.

func attemptRecovery(optionIndex recoveryOptionIndex: Int
,
                       
andThen resultHandler: (recovered: Bool) -> Void
)

/// Attempt to recover from this error when the user selected the

/// option at the given index. Returns true to indicate

/// successful recovery, and false otherwise.

///

/// This entry point is used for recovery of errors handled at

/// the "application" granularity, where nothing else in the

/// application can proceed until the attmpted error recovery

/// completes.

func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool

}

extension
RecoverableError {
  
/// By default, implements document-modal recovery via application-model

/// recovery.

func attemptRecovery(optionIndex recoveryOptionIndex: Int
,
                       
andThen resultHandler: (Bool) -> Void
) {
    resultHandler(recovered: attemptRecovery(optionIndex: recoveryOptionIndex))
  }
}

Error types that conform to RecoverableError may be given an opportunity to recover from the error. The user can be presented with some number of (localized) recovery options, described by recoveryOptions, and the selected option will be passed to the appropriate attemptRecovery method.

The CustomNSError protocol describes an error that wants to provide custom NSError information. This can be used, e.g., to provide a specific domain/code or to populate NSError's userInfo dictionary with values for custom keys that can be accessed from Objective-C code but are not covered by the other protocols.

/// Describes an error type that fills in the userInfo directly.
protocol CustomNSError :
Error {
  
var errorDomain: String { get
}
  
var errorCode: Int { get
}
  
var errorUserInfo: [String : AnyObject] { get
}
}

Note that, unlike with NSError, the provided errorUserInfo requires String keys. This is in line with common practice for NSError and is important for the implementation (see below). All of these properties are defaulted, so one can provide any subset:

extension
CustomNSError {
  
var errorDomain: String { ...
}
  
var errorCode: Int { ...
}
  
var errorUserInfo: [String : AnyObject] { ...
}
}

Mapping error types to NSError

Every type that conforms to the Error protocol is implicitly bridged to NSError. This has been the case since Swift 2, where the compiler provides a domain (i.e., the mangled name of the type) and code (based on the discriminator of the enumeration type). This proposal also allows for the userInfo dictionary to be populated by the runtime, which will check for conformance to the various protocols (LocalizedError, RecoverableError, or CustomNSError) to retrieve information.

Conceptually, this could be implemented by eagerly creating a userInfo dictionary for a given instance of Error:

func createUserInfo(error: Error) -> [NSObject : AnyObject
] {
  
var userInfo: [NSObject : AnyObject] = [:
]

// Retrieve custom userInfo information.

if let customUserInfoError = error as?
CustomNSError {
    userInfo
= customUserInfoError.
userInfo
  }

if let localizedError = error as?
LocalizedError {
    
if let description = localizedError.
errorDescription {
      userInfo[NSLocalizedDescriptionKey]

description
    }

if let reason = localizedError.
failureReason {
      userInfo[NSLocalizedFailureReasonErrorKey]

reason
    }

if let suggestion = localizedError.
recoverySuggestion {
      userInfo[NSLocalizedRecoverySuggestionErrorKey]

suggestion
    }

if let helpAnchor = localizedError.
helpAnchor {
      userInfo[NSHelpAnchorErrorKey]

helpAnchor
    }
  }

if let recoverableError = error as?
RecoverableError {
    userInfo[NSLocalizedRecoveryOptionsErrorKey]
= recoverableError.
recoveryOptions
    userInfo[NSRecoveryAttempterErrorKey]

RecoveryAttempter()
  }
}

The RecoveryAttempter class is an implementation detail. It will implement the informal protocol NSErrorRecoveryAttempting for the given error:

class RecoveryAttempter :
NSObject {
  
@objc
(attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo:)
  
func attemptRecovery(fromError nsError
: NSError,
                       
optionIndex recoveryOptionIndex: Int
,
                       delegate:
AnyObject
?,
                       didRecoverSelector: Selector,
                       contextInfo:
UnsafeMutablePointer<Void>
) {
    
let error = nsError as!
RecoverableError
    error
.attemptRecovery(optionIndex: recoveryOptionIndex) { success in

// Exposition only: this part will actually have to be

// implemented in Objective-C to pass the BOOL and void* through.

      delegate?
.
perform(didRecoverSelector, with: success, with: contextInfo)
    }
  }

@objc
(attemptRecoveryFromError:optionIndex:)
  
func attemptRecovery(fromError nsError
: NSError,
                       
optionIndex recoveryOptionIndex: Int) -> Bool
{
    
let error = nsError as!
RecoverableError
    
return error.
attemptRecovery(optionIndex: recoveryOptionIndex)
  }
}

The actual the population of the userInfo dictionary should not be eager. NSError provides the notion of global "user info value providers" that it uses to lazily request the values for certain keys, via setUserInfoValueProvider(forDomain:provider:), which is declared as:

extension
NSError {
  
@available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *
)
  
class func setUserInfoValueProvider(forDomain errorDomain: String
,
                                      provider: ((NSError,
String) -> AnyObject
?)? = nil)
}

The runtime would need to register a user info value provider for each error type the first time it is bridged into NSError, supplying the domain and the following user info value provider function:

func userInfoValueProvider(nsError: NSError, key: String) -> AnyObject
? {
  
let error = nsError as!
Error
  
switch
key {
  
case
NSLocalizedDescriptionKey:
    
return (error as? LocalizedError)?.
errorDescription

case
NSLocalizedFailureReasonErrorKey:
    
return (error as? LocalizedError)?.
failureReason

case
NSLocalizedRecoverySuggestionErrorKey:
    
return (error as? LocalizedError)?.
recoverySuggestion

case
NSHelpAnchorErrorKey:
    
return (error as? LocalizedError)?.
helpAnchor

case
NSLocalizedRecoveryOptionsErrorKey:
    
return (error as? RecoverableError)?.
recoveryOptions

case
NSRecoveryAttempterErrorKey:
    
return error is RecoverableError ? RecoveryAttempter() : nil

default
:
    
guard let customUserInfoError = error as? CustomNSError else { return nil
}
    
return customUserInfoError.
userInfo[key]
  }
}

On platforms that predate the introduction of user info value providers, there are alternate implementation strategies, including introducing a custom NSDictionary subclass to use as the userInfo in the NSError that lazily populates the dictionary by, effectively, calling the userInfoValueProvider function above for each requested key. Or, one could eagerly populate userInfo on older platforms.

Importing error types from Objective-C

In Objective-C, error domains are typically constructed using an enumeration describing the error codes and a constant describing the error domain, e.g,

extern NSString *const
AVFoundationErrorDomain;

typedef NS_ENUM(NSInteger
, AVError) {
  AVErrorUnknown = -
11800
,
  AVErrorOutOfMemory = -
11801
,
  AVErrorSessionNotRunning = -
11803
,
  AVErrorDeviceAlreadyUsedByAnotherSession = -
11804
,
  
// ...

}

This is currently imported as an enum that conforms to Error:

enum AVError : Int
{
  
case unknown = -11800

case outOfMemory = -11801

case sessionNotRunning = -11803

case deviceAlreadyUsedByAnotherSession = -11804

static var _domain: String { return
AVFoundationErrorDomain }
}

and Swift code introduces an extension that makes it an Error, along with some implementation magic to allow bridging from an NSError (losing userInfo in the process):

extension AVError :
Error {
  
static var _domain: String { return
AVFoundationErrorDomain }
}

Instead, error enums should be expressed with a new macro, NS_ERROR_ENUM, that ties together the code and domain in the Objective-C header:

extern NSString *const
AVFoundationErrorDomain;

typedef NS_ERROR_ENUM(NSInteger
, AVError, AVFoundationErrorDomain) {
  AVErrorUnknown = -
11800
,
  AVErrorOutOfMemory = -
11801
,
  AVErrorSessionNotRunning = -
11803
,
  AVErrorDeviceAlreadyUsedByAnotherSession = -
11804
,
  
// ...

}

This will import as a new struct AVError that contains an NSError, so there is no information loss. The actual enum will become a nested type Code, so that it is still accessible. The resulting struct will be as follows:

struct
AVError {
  
/// Stored NSError. Note that error.domain == AVFoundationErrorDomain is an invariant.

private var
error: NSError

/// Describes the error codes; directly imported from AVError

enum Code : Int
, ErrorCodeProtocol {
    
typealias ErrorType =
AVError

case unknown = -11800

case outOfMemory = -11801

case sessionNotRunning = -11803

case deviceAlreadyUsedByAnotherSession = -11804

func errorMatchesCode(_ error: AVError) -> Bool
{
      
return error.code == self

    }
  }

/// Allow one to create an error (optionally) with a userInfo dictionary.

init(_ code: Code, userInfo: [NSObject: AnyObject] = [:
]) {
    error
= NSError(code: code.rawValue
, domain: _domain, userInfo: userInfo)
  }

/// Retrieve the code.

var code: Code { return Code(rawValue: error.code)!
}

/// Allow direct access to the userInfo dictionary.

var userInfo: [NSObject: AnyObject] { return error.
userInfo }

/// Make it easy to refer to constants without context.

static let unknown: Code = .
unknown
  
static let outOfMemory: Code = .
outOfMemory
  
static let sessionNotRunning: Code = .
sessionNotRunning
  
static let deviceAlreadyUsedByAnotherSession: Code = .
deviceAlreadyUsedByAnotherSession
}

// Implementation detail: makes AVError conform to Error
extension AVError :
Error {
  
static var _domain: String { return
AVFoundationErrorDomain }

var _code: Int { return error.
code }
}

This syntax allows one to throw specific errors fairly easily, with or without userInfo dictionaries:

throw AVError(.
sessionNotRunning)

throw AVError(.sessionNotRunning, userInfo: [ ... ])
The ImportedErrorCode protocol is a helper so that we can define a general ~= operator, which is used by both switchcase matching and catch blocks:

protocol
ErrorCodeProtocol {
  
typealias ErrorType :
Error

func errorMatchesCode(_ error: ErrorType) -> Bool

}

func ~= <EC: ErrorCodeProtocol> (error: Error, code: EC) -> Bool
{
  
guard let myError = error as? EC.ErrorType else { return false
}
  
return code.
errorMatchesCode(myError)
}

Mapping NSError types back into Swift

When an NSError object bridged to an Error instance, it may be immediately mapped back to a Swift error type (e.g., if the error was created as a HomeworkError instance in Swift and then passed through NSError unmodified) or it might be leave as an instance of NSError. The error might then be catch as a particular Swift error type, e.g.,

catch let error as AVError where error.code == .
sessionNotRunning {
  
// able to access userInfo here!

}

In this case, the mapping from an NSError instance to AVError goes through an implementation-detail protocol_ObjectiveCBridgeableError:

protocol _ObjectiveCBridgeableError :
Error {
  
/// Produce a value of the error type corresponding to the given NSError,

/// or return nil if it cannot be bridged.

init
?(_bridgedNSError error: NSError)
}

The initializer is responsible for checking the domain and (optionally) the code of the incoming NSError to map it to an instance of the Swift error type. For example, AVError would adopt this protocol as follows:

// Implementation detail: makes AVError conform to _ObjectiveCBridgeableError
extension AVError :
_ObjectiveCBridgeableError {
  
init
?(_bridgedNSError error: NSError) {
    
// Check whether the error comes from the AVFoundation error domain

if error.domain != AVFoundationErrorDomain { return nil
}

// Save the error

self.error =
error
  }
}

We do not propose that _ObjectiveCBridgeableError become a public protocol, because the core team has already deferred a similar proposal (SE-0058) to make the related protocol _ObjectiveCBridgeable public.

Other Issues

NSError codes and domains are important for localization of error messages. This is barely supported today by genstrings, but becomes considerably harder when the domain and code are hidden (as they are in Swift). We would need to consider tooling to make it easier to localize error descriptions, recovery options, etc. in a sensible way. Although this is out of the scope of the Swift language per se, it's an important part of the developer story.

Impact on existing code

This is a major source-breaking change for Objective-C APIs that operate on NSError values, because those parameter/return/property types will change from NSError to Error. There are ~400 such APIs in the macOS SDK, and closer to 500 in the iOS SDK, which is a sizable number. Fortunately, this is similar in scope to the Foundation value types proposal, and can use the same code migration mechanism. That said, the scale of this change means that it should either happen in Swift 3 or not at all.

Future directions

Better tooling for describing errors

When adopting one of the new protocols (e.g., LocalizedError) in an enum, one will inevitably end up with a number of switch statements that have to enumerate all of the cases, leading to a lot of boilerplate. Better tooling could improve the situation considerably: for example, one could use something like Cocoa's stringsdict files to provide localized strings identified by the enum name, case name, and property. That would eliminate the need for the switch-on-all-cases implementations of each property.

Round-tripping errors through userInfo

The CustomNSError protocol allows one to place arbitrary key/value pairs into NSError's userInfo dictionary. The implementation-detail _ObjectiveCBridgeableError protocol allows one to control how a raw NSError is mapped to a particular error type. One could effectively serialize the entire state of a particular error type into the userInfo dictionary viaCustomNSError, then restore it via _ObjectiveCBridgeableError, allowing one to form a complete NSError in Objective-C that can reconstitute itself as a particular Swift error type, which can be useful both for mixed-source projects and (possibly) as a weak form of serialization for NSErrors.

Alternatives considered

Exposing the domain, code, and user-info dictionary directly

This proposal does not directly expose the domain, code, or user-info dictionary on ErrorProtocol, because these notions are superseded by Swift's strong typing of errors. The domain is effectively subsumed by the type of the error (e.g., a Swift-defined error type uses its mangled name as the domain); the code is some type-specific value (e.g., the discriminator of the enum); and the user-info dictionary is an untyped set of key-value pairs that are better expressed in Swift as data on the specific error type.

Bridging NSError to a new value type Error

One could introduce a new value type, Error, that stores a domain, code, and user-info dictionary but provides them with value semantics. Doing so would make it easier to create "generic" errors that carry some information. However, we feel that introducing new error types in Swift is already easier than establishing a new domain and a set of codes, because a new enum type provides this information naturally in Swift.


(Shawn Erickson) #4

I did a quick read and this looks great! Thanks to you two for pulling this
together.

I will attempt a deeper read and comment as needed later today. I am
interested in helping with this as possible.

-Shawn

···

On Mon, Jun 27, 2016 at 2:41 PM Douglas Gregor via swift-evolution < swift-evolution@swift.org> wrote:

Hi all,

Proposal link:
https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md

Here is a detailed proposal draft for bridging NSError to ErrorProtocol.
Getting this right is surprisingly involved, so the detailed design on this
proposal is fairly large. Comments welcome!

- Doug

NSError Bridging

   - Proposal: SE-NNNN
   <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/NNNN-nserror-bridging.md>
   - Author: Doug Gregor <https://github.com/DougGregor>, Charles Srstka
   <https://github.com/CharlesJS>
   - Status: Awaiting review
   - Review manager: TBD

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#introduction>
Introduction

Swift's error handling model interoperates directly with Cocoa's NSError
conventions. For example, an Objective-C method with an NSError** parameter,
e.g.,

- (nullable NSURL *)replaceItemAtURL:(NSURL *)url options:(NSFileVersionReplacingOptions)options error:(NSError **)error;

will be imported as a throwing method:

func replaceItem(at url: URL, options: ReplacingOptions = []) throws -> URL

Swift bridges between ErrorProtocol-conforming types and NSError so, for
example, a Swift enum that conforms toErrorProtocol can be thrown and
will be reflected as an NSError with a suitable domain and code.
Moreover, an NSErrorproduced with that domain and code can be caught as
the Swift enum type, providing round-tripping so that Swift can deal in
ErrorProtocol values while Objective-C deals in NSError objects.

However, the interoperability is incomplete in a number of ways, which
results in Swift programs having to walk a careful line between the
ErrorProtocol-based Swift way and the NSError-based way. This proposal
attempts to bridge those gaps.

Swift-evolution thread: Charles Srstka's pitch for Consistent bridging
for NSErrors at the language boundary
<https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160502/016618.html>,
which discussed Charles' original proposal
<https://github.com/apple/swift-evolution/pull/331> that addressed these
issues by providing NSError to ErrorProtocol bridging and exposing the
domain, code, and user-info dictionary for all errors. This proposal
expands upon that work, but without directly exposing the domain, code, and
user-info.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#motivation>
Motivation

There are a number of weaknesses in Swift's interoperability with Cocoa's
error model, including:

   1.

   There is no good way to provide important error information when
   throwing an error from Swift. For example, let's consider a simple
   application-defined error in Swift:

   enum HomeworkError : Int, ErrorProtocol {
     case forgotten
     case lost
     case dogAteIt
   }

   One can throw HomeworkError.dogAteIt and it can be interpreted as an
   NSError by Objective-C with an appropriate error domain (effectively,
   the mangled name of the HomeworkError type) and code (effectively, the
   case discriminator). However, one cannot provide a localized description,
   help anchor, recovery attempter, or any other information commonly placed
   into the userInfo dictionary of an NSError. To provide these values,
   one must specifically construct an NSError in Swift, e.g.,

   throw NSError(code: HomeworkError.dogAteIt.rawValue,
                 domain: HomeworkError._domain,
                 userInfo: [ NSLocalizedDescriptionKey : "the dog ate it" ])

   2.

   There is no good way to get information typically associated with
   NSError's userInfo in Swift. For example, the Swift-natural way to
   catch a specific error in the AVError error domain doesn't give one
   access to the userInfo dictionary, e.g.,:

   catch let error as AVError where error == .diskFull {
     // AVError is an enum, so one only gets the equivalent of the code.
     // There is no way to access the localized description (for example) or
     // any other information typically stored in the ``userInfo`` dictionary.
   }

   The workaround is to catch as an NSError, which is quite a bit more
   ugly:

   catch let error as NSError where error._domain == AVFoundationErrorDomain && error._code == AVFoundationErrorDomain.diskFull.rawValue {
     // okay: userInfo is finally accessible, but still weakly typed
   }

   This makes it extremely hard to access common information, such as the
   localized description. Moreover, the userInfo dictionary is
   effectively untyped so, for example, one has to know a priori that the
   value associated with the known AVErrorDeviceKey will be typed as
   CMTime:

   catch let error as NSError where error._domain = AVFoundationErrorDomain {
     if let time = error.userInfo[AVErrorDeviceKey] as? CMTime {
       // ...
     }
   }

   It would be far better if one could catch an AVError directly and
   query the time in a type-safe manner:

   catch let error as AVError {
     if let time = error.time {
       // ...
     }
   }

   3.

   NSError is inconsistently bridged with ErrorProtocol. Swift
   interoperates by translating between NSError and ErrorProtocol when
   mapping between a throwing Swift method/initializer and an Objective-C
   method with an NSError** parameter. However, an Objective-C method
   that takes an NSError* parameter (e.g., to render it) does not bridge
   to ErrorProtocol, meaning that NSError is part of the API in Swift in
   some places (but not others). For example, NSError leaks through when
   the following UIDocument API in Objective-C:

   - (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;

   is imported into Swift as follows:

   func handleError(_ error: NSError, userInteractionPermitted: Bool)

   One would expect the first parameter to be imported as ErrorProtocol.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#proposed-solution>Proposed
solution

This proposal involves directly addressing (1)-(3) with new protocols and
a different way of bridging Objective-C error code types into Swift, along
with some conveniences for working with Cocoa errors:

   1.

   Introduce three new protocols for describing more information about
   errors: LocalizedError, RecoverableError, andCustomNSError. For
   example, an error type can provide a localized description by conforming to
   LocalizedError:

   extension HomeworkError : LocalizedError {
     var errorDescription: String? {
       switch self {
       case .forgotten: return NSLocalizedString("I forgot it")
       case .lost: return NSLocalizedString("I lost it")
       case .dogAteIt: return NSLocalizedString("The dog ate it")
       }
     }
   }

   2.

   Imported Objective-C error types should be mapped to struct types that
   store an NSError so that no information is lost when bridging from an
   NSError to the Swift error types. We propose to introduce a new macro,
   NS_ERROR_ENUM, that one can use to both declare an enumeration type
   used to describe the error codes as well as tying that type to a specific
   domain constant, e.g.,

   typedef NS_ERROR_ENUM(NSInteger, AVError, AVFoundationErrorDomain) {
     AVErrorUnknown = -11800,
     AVErrorOutOfMemory = -11801,
     AVErrorSessionNotRunning = -11803,
     AVErrorDeviceAlreadyUsedByAnotherSession = -11804,
     // ...
   }

   The imported AVError will have a struct that allows one to access the
   userInfo dictionary directly. This retains the ability to catch via a
   specific code, e.g.,

   catch AVError.outOfMemory {
    // ...
   }

   However, catching a specific error as a value doesn't lose information:

   catch let error as AVError where error.code == .sessionNotRunning {
    // able to access userInfo here!
   }

   This also gives the ability for one to add typed accessors for known
   keys within the userInfo dictionary:

   extension AVError {
    var time: CMTime? {
      get {
        return userInfo[AVErrorTimeKey] as? CMTime?
      }

      set {
        userInfo[AVErrorTimeKey] = newValue.map { $0 as CMTime }
      }
    }
   }

   3.

   Bridge NSError to ErrorProtocol, so that all NSError uses are bridged
   consistently. For example, this means that the Objective-C API:

   - (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;

   is imported into Swift as:

   func handleError(_ error: ErrorProtocol, userInteractionPermitted: Bool)

   This will use the same bridging logic in the Clang importer that we
   use for other value types (Array, String, URL, etc.), but with the
   runtime translation we've already been doing for catching/throwing errors.

   When we introduce this bridging, we will need to remove NSError's
   conformance to ErrorProtocol to avoid creating cyclic implicit
   conversions. However, we still need an easy way to create an
   ErrorProtocol instance from an arbitrary NSError, e.g.,

   extension NSError {
     var asError: ErrorProtocol { ... }
   }

   4.

   In Foundation, add an extension to ErrorProtocol that provides typed
   access to the common user-info keys. Note that we focus only on those
   user-info keys that are read by user code (vs. only accessed by frameworks):

   extension ErrorProtocol {
     // Note: for exposition only. Not actual API.
     private var userInfo: [NSObject : AnyObject] {
       return (self as! NSError).userInfo
     }

     var localizedDescription: String {
       return (self as! NSError).localizedDescription
     }

     var filePath: String? {
       return userInfo[NSFilePathErrorKey] as? String
     }

     var stringEncoding: String.Encoding? {
       return (userInfo[NSStringEncodingErrorKey] as? NSNumber)
                .map { String.Encoding(rawValue: $0.uintValue) }
     }

     var underlying: ErrorProtocol? {
       return (userInfo[NSUnderlyingErrorKey] as? NSError)?.asError
     }

     var url: URL? {
       return userInfo[NSURLErrorKey] as? URL
     }
   }

   5.

   Rename ErrorProtocol to Error: once we've completed the bridging
   story, Error becomes the primary way to work with error types in
   Swift, and the value type to which NSError is bridged:

   func handleError(_ error: Error, userInteractionPermitted: Bool)

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#detailed-design>Detailed
design

This section details both the design (including the various new protocols,
mapping from Objective-C error code enumeration types into Swift types,
etc.) and the efficient implementation of this design to interoperate with
NSError. Throughout the detailed design, we already assume the name
change from ErrorProtocol to Error.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#new-protocols>New
protocols

This proposal introduces several new protocols that allow error types to
expose more information about error types.

The LocalizedError protocol describes an error that provides localized
messages for display to the end user, all of which provide default
implementations. The conforming type can provide implementations for any
subset of these requirements:

protocol LocalizedError : Error {
  /// A localized message describing what error occurred.
  var errorDescription: String? { get }

  /// A localized message describing the reason for the failure.
  var failureReason: String? { get }

  /// A localized message describing how one might recover from the failure.
  var recoverySuggestion: String? { get }

  /// A localized message providing "help" text if the user requests help.
  var helpAnchor: String? { get }
}
extension LocalizedError {
  var errorDescription: String? { return nil }
  var failureReason: String? { return nil }
  var recoverySuggestion: String? { return nil }
  var helpAnchor: String? { return nil }
}

The RecoverableError protocol describes an error that might be
recoverable:

protocol RecoverableError : Error {
  /// Provides a set of possible recovery options to present to the user.
  var recoveryOptions: [String] { get }

  /// Attempt to recover from this error when the user selected the
  /// option at the given index. This routine must call resultHandler and
  /// indicate whether recovery was successful (or not).
  ///
  /// This entry point is used for recovery of errors handled at a
  /// "document" granularity, that do not affect the entire
  /// application.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int,
                       andThen resultHandler: (recovered: Bool) -> Void)

  /// Attempt to recover from this error when the user selected the
  /// option at the given index. Returns true to indicate
  /// successful recovery, and false otherwise.
  ///
  /// This entry point is used for recovery of errors handled at
  /// the "application" granularity, where nothing else in the
  /// application can proceed until the attmpted error recovery
  /// completes.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool
}
extension RecoverableError {
  /// By default, implements document-modal recovery via application-model
  /// recovery.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int,
                       andThen resultHandler: (Bool) -> Void) {
    resultHandler(recovered: attemptRecovery(optionIndex: recoveryOptionIndex))
  }
}

Error types that conform to RecoverableError may be given an opportunity
to recover from the error. The user can be presented with some number of
(localized) recovery options, described by recoveryOptions, and the
selected option will be passed to the appropriate attemptRecovery method.

The CustomNSError protocol describes an error that wants to provide
custom NSError information. This can be used, e.g., to provide a specific
domain/code or to populate NSError's userInfo dictionary with values for
custom keys that can be accessed from Objective-C code but are not covered
by the other protocols.

/// Describes an error type that fills in the userInfo directly.protocol CustomNSError : Error {
  var errorDomain: String { get }
  var errorCode: Int { get }
  var errorUserInfo: [String : AnyObject] { get }
}

Note that, unlike with NSError, the provided errorUserInfo requires String keys.
This is in line with common practice for NSError and is important for the
implementation (see below). All of these properties are defaulted, so one
can provide any subset:

extension CustomNSError {
  var errorDomain: String { ... }
  var errorCode: Int { ... }
  var errorUserInfo: [String : AnyObject] { ... }
}

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#mapping-error-types-to-nserror>Mapping
error types to NSError

Every type that conforms to the Error protocol is implicitly bridged to
NSError. This has been the case since Swift 2, where the compiler
provides a domain (i.e., the mangled name of the type) and code (based on
the discriminator of the enumeration type). This proposal also allows for
the userInfo dictionary to be populated by the runtime, which will check
for conformance to the various protocols (LocalizedError, RecoverableError,
or CustomNSError) to retrieve information.

Conceptually, this could be implemented by eagerly creating a userInfo dictionary
for a given instance of Error:


(Dmitri Gribenko) #5

I'm not sure I really want '.url' and '.stringEncoding' on every
Error. 'var underlying' is universally useful, but providing it
requires a implementing conformance to CustomNSError, which has to
vend a weakly-typed dictionary. Is this really the API we want to
expose?

Also, the underlying error has to be stored somewhere, which
effectively prevents implementers of CustomNSError from being enums.

Dmitri

···

On Mon, Jun 27, 2016 at 11:17 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

extension ErrorProtocol {
  // Note: for exposition only. Not actual API.
  private var userInfo: [NSObject : AnyObject] {
    return (self as! NSError).userInfo
  }

  var localizedDescription: String {
    return (self as! NSError).localizedDescription
  }

  var filePath: String? {
    return userInfo[NSFilePathErrorKey] as? String
  }

  var stringEncoding: String.Encoding? {
    return (userInfo[NSStringEncodingErrorKey] as? NSNumber)
             .map { String.Encoding(rawValue: $0.uintValue) }
  }

  var underlying: ErrorProtocol? {
    return (userInfo[NSUnderlyingErrorKey] as? NSError)?.asError
  }

  var url: URL? {
    return userInfo[NSURLErrorKey] as? URL
  }
}

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/


(Paul Cantrell) #6

The LocalizedError protocol describes an error that provides localized messages for display to the end user, all of which provide default implementations. The conforming type can provide implementations for any subset of these requirements:

protocol LocalizedError : Error {
  /// A localized message describing what error occurred.
  var errorDescription: String? { get }
  …
}

Given that LocalizedError would now be its own protocol that not all errors would conform to, could errorDescription be non-optional?

  var errorDescription: String { get }

It would be nice if conformance to LocalizedError guaranteed the presence of a user-readable message. Such a guarantee is useful when building a UI.

I realize the bridging to NSError may make this impossible, but in principle it seems like the right design.

Cheers,

Paul

···

On Jun 27, 2016, at 1:17 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:


(Brent Royal-Gordon) #7

Note that, unlike with NSError, the provided errorUserInfo requires String keys.

Is there any way this could be tightened further to require Error.UserInfoKey keys (where Error.UserInfoKey is a Notification.Name-style wrapper)?

···

--
Brent Royal-Gordon
Architechies


(Jeff Kelley) #8

I agree with Charles about removing the “NSError” bit from the name, but otherwise this looks fantastic. By my “would I be excited to use this in Swift” metric, this is definitely a welcome addition.

Jeff Kelley

SlaunchaMan@gmail.com | @SlaunchaMan <https://twitter.com/SlaunchaMan> | jeffkelley.org <http://jeffkelley.org/>

···

On Jun 27, 2016, at 4:54 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

Obviously, I’m in favor of this one. +1!

I think I did prefer the older name of CustomUserInfoError for the domain/code/userInfo protocol, rather than CustomNSError. This is just because I’d like to be able to do a global search through my project for “NSError” and have it turn up empty. Maybe a silly reason, I know. :wink:

I sent a few more comments off-list before I realized you’d posted the proposal already, so you can peruse, consider, and/or discard those at your leisure. It’s nothing terribly important, though; this proposal is pretty great as is.

Thanks so much for doing this! I’m really excited for better error handling in Swift 3.

Charles

On Jun 27, 2016, at 1:17 PM, Douglas Gregor <dgregor@apple.com <mailto:dgregor@apple.com>> wrote:

Hi all,

Proposal link: https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md

Here is a detailed proposal draft for bridging NSError to ErrorProtocol. Getting this right is surprisingly involved, so the detailed design on this proposal is fairly large. Comments welcome!

  - Doug

NSError Bridging

Proposal: SE-NNNN <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/NNNN-nserror-bridging.md>
Author: Doug Gregor <https://github.com/DougGregor>, Charles Srstka <https://github.com/CharlesJS>
Status: Awaiting review
Review manager: TBD
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#introduction>Introduction

Swift's error handling model interoperates directly with Cocoa's NSError conventions. For example, an Objective-C method with an NSError** parameter, e.g.,

- (nullable NSURL *)replaceItemAtURL:(NSURL *)url options:(NSFileVersionReplacingOptions)options error:(NSError **)error;
will be imported as a throwing method:

func replaceItem(at url: URL, options: ReplacingOptions = []) throws -> URL
Swift bridges between ErrorProtocol-conforming types and NSError so, for example, a Swift enum that conforms toErrorProtocol can be thrown and will be reflected as an NSError with a suitable domain and code. Moreover, an NSErrorproduced with that domain and code can be caught as the Swift enum type, providing round-tripping so that Swift can deal in ErrorProtocol values while Objective-C deals in NSError objects.

However, the interoperability is incomplete in a number of ways, which results in Swift programs having to walk a careful line between the ErrorProtocol-based Swift way and the NSError-based way. This proposal attempts to bridge those gaps.

Swift-evolution thread: Charles Srstka's pitch for Consistent bridging for NSErrors at the language boundary <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160502/016618.html>, which discussed Charles' original proposal <https://github.com/apple/swift-evolution/pull/331> that addressed these issues by providing NSError to ErrorProtocol bridging and exposing the domain, code, and user-info dictionary for all errors. This proposal expands upon that work, but without directly exposing the domain, code, and user-info.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#motivation>Motivation

There are a number of weaknesses in Swift's interoperability with Cocoa's error model, including:

There is no good way to provide important error information when throwing an error from Swift. For example, let's consider a simple application-defined error in Swift:

enum HomeworkError : Int, ErrorProtocol {
  case forgotten
  case lost
  case dogAteIt
}
One can throw HomeworkError.dogAteIt and it can be interpreted as an NSError by Objective-C with an appropriate error domain (effectively, the mangled name of the HomeworkError type) and code (effectively, the case discriminator). However, one cannot provide a localized description, help anchor, recovery attempter, or any other information commonly placed into the userInfo dictionary of an NSError. To provide these values, one must specifically construct an NSError in Swift, e.g.,

throw NSError(code: HomeworkError.dogAteIt.rawValue,
              domain: HomeworkError._domain,
              userInfo: [ NSLocalizedDescriptionKey : "the dog ate it" ])
There is no good way to get information typically associated with NSError's userInfo in Swift. For example, the Swift-natural way to catch a specific error in the AVError error domain doesn't give one access to the userInfo dictionary, e.g.,:

catch let error as AVError where error == .diskFull {
  // AVError is an enum, so one only gets the equivalent of the code.
  // There is no way to access the localized description (for example) or
  // any other information typically stored in the ``userInfo`` dictionary.
}
The workaround is to catch as an NSError, which is quite a bit more ugly:

catch let error as NSError where error._domain == AVFoundationErrorDomain && error._code == AVFoundationErrorDomain.diskFull.rawValue {
  // okay: userInfo is finally accessible, but still weakly typed
}
This makes it extremely hard to access common information, such as the localized description. Moreover, the userInfo dictionary is effectively untyped so, for example, one has to know a priori that the value associated with the known AVErrorDeviceKey will be typed as CMTime:

catch let error as NSError where error._domain = AVFoundationErrorDomain {
  if let time = error.userInfo[AVErrorDeviceKey] as? CMTime {
    // ...
  }
}
It would be far better if one could catch an AVError directly and query the time in a type-safe manner:

catch let error as AVError {
  if let time = error.time {
    // ...
  }
}
NSError is inconsistently bridged with ErrorProtocol. Swift interoperates by translating between NSError and ErrorProtocol when mapping between a throwing Swift method/initializer and an Objective-C method with an NSError** parameter. However, an Objective-C method that takes an NSError* parameter (e.g., to render it) does not bridge to ErrorProtocol, meaning that NSError is part of the API in Swift in some places (but not others). For example, NSError leaks through when the following UIDocument API in Objective-C:

- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
is imported into Swift as follows:

func handleError(_ error: NSError, userInteractionPermitted: Bool)
One would expect the first parameter to be imported as ErrorProtocol.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#proposed-solution>Proposed solution

This proposal involves directly addressing (1)-(3) with new protocols and a different way of bridging Objective-C error code types into Swift, along with some conveniences for working with Cocoa errors:

Introduce three new protocols for describing more information about errors: LocalizedError, RecoverableError, andCustomNSError. For example, an error type can provide a localized description by conforming to LocalizedError:

extension HomeworkError : LocalizedError {
  var errorDescription: String? {
    switch self {
    case .forgotten: return NSLocalizedString("I forgot it")
    case .lost: return NSLocalizedString("I lost it")
    case .dogAteIt: return NSLocalizedString("The dog ate it")
    }
  }
}
Imported Objective-C error types should be mapped to struct types that store an NSError so that no information is lost when bridging from an NSError to the Swift error types. We propose to introduce a new macro, NS_ERROR_ENUM, that one can use to both declare an enumeration type used to describe the error codes as well as tying that type to a specific domain constant, e.g.,

typedef NS_ERROR_ENUM(NSInteger, AVError, AVFoundationErrorDomain) {
  AVErrorUnknown = -11800,
  AVErrorOutOfMemory = -11801,
  AVErrorSessionNotRunning = -11803,
  AVErrorDeviceAlreadyUsedByAnotherSession = -11804,
  // ...
}
The imported AVError will have a struct that allows one to access the userInfo dictionary directly. This retains the ability to catch via a specific code, e.g.,

catch AVError.outOfMemory {
// ...
}
However, catching a specific error as a value doesn't lose information:

catch let error as AVError where error.code == .sessionNotRunning {
// able to access userInfo here!
}
This also gives the ability for one to add typed accessors for known keys within the userInfo dictionary:

extension AVError {
var time: CMTime? {
   get {
     return userInfo[AVErrorTimeKey] as? CMTime?
   }

   set {
     userInfo[AVErrorTimeKey] = newValue.map { $0 as CMTime }
   }
}
}
Bridge NSError to ErrorProtocol, so that all NSError uses are bridged consistently. For example, this means that the Objective-C API:

- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
is imported into Swift as:

func handleError(_ error: ErrorProtocol, userInteractionPermitted: Bool)
This will use the same bridging logic in the Clang importer that we use for other value types (Array, String, URL, etc.), but with the runtime translation we've already been doing for catching/throwing errors.

When we introduce this bridging, we will need to remove NSError's conformance to ErrorProtocol to avoid creating cyclic implicit conversions. However, we still need an easy way to create an ErrorProtocol instance from an arbitrary NSError, e.g.,

extension NSError {
  var asError: ErrorProtocol { ... }
}
In Foundation, add an extension to ErrorProtocol that provides typed access to the common user-info keys. Note that we focus only on those user-info keys that are read by user code (vs. only accessed by frameworks):

extension ErrorProtocol {
  // Note: for exposition only. Not actual API.
  private var userInfo: [NSObject : AnyObject] {
    return (self as! NSError).userInfo
  }

  var localizedDescription: String {
    return (self as! NSError).localizedDescription
  }

  var filePath: String? {
    return userInfo[NSFilePathErrorKey] as? String
  }

  var stringEncoding: String.Encoding? {
    return (userInfo[NSStringEncodingErrorKey] as? NSNumber)
             .map { String.Encoding(rawValue: $0.uintValue) }
  }

  var underlying: ErrorProtocol? {
    return (userInfo[NSUnderlyingErrorKey] as? NSError)?.asError
  }

  var url: URL? {
    return userInfo[NSURLErrorKey] as? URL
  }
}
Rename ErrorProtocol to Error: once we've completed the bridging story, Error becomes the primary way to work with error types in Swift, and the value type to which NSError is bridged:

func handleError(_ error: Error, userInteractionPermitted: Bool)
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#detailed-design>Detailed design

This section details both the design (including the various new protocols, mapping from Objective-C error code enumeration types into Swift types, etc.) and the efficient implementation of this design to interoperate with NSError. Throughout the detailed design, we already assume the name change from ErrorProtocol to Error.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#new-protocols>New protocols

This proposal introduces several new protocols that allow error types to expose more information about error types.

The LocalizedError protocol describes an error that provides localized messages for display to the end user, all of which provide default implementations. The conforming type can provide implementations for any subset of these requirements:

protocol LocalizedError : Error {
  /// A localized message describing what error occurred.
  var errorDescription: String? { get }

  /// A localized message describing the reason for the failure.
  var failureReason: String? { get }

  /// A localized message describing how one might recover from the failure.
  var recoverySuggestion: String? { get }

  /// A localized message providing "help" text if the user requests help.
  var helpAnchor: String? { get }
}

extension LocalizedError {
  var errorDescription: String? { return nil }
  var failureReason: String? { return nil }
  var recoverySuggestion: String? { return nil }
  var helpAnchor: String? { return nil }
}
The RecoverableError protocol describes an error that might be recoverable:

protocol RecoverableError : Error {
  /// Provides a set of possible recovery options to present to the user.
  var recoveryOptions: [String] { get }

  /// Attempt to recover from this error when the user selected the
  /// option at the given index. This routine must call resultHandler and
  /// indicate whether recovery was successful (or not).
  ///
  /// This entry point is used for recovery of errors handled at a
  /// "document" granularity, that do not affect the entire
  /// application.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int,
                       andThen resultHandler: (recovered: Bool) -> Void)

  /// Attempt to recover from this error when the user selected the
  /// option at the given index. Returns true to indicate
  /// successful recovery, and false otherwise.
  ///
  /// This entry point is used for recovery of errors handled at
  /// the "application" granularity, where nothing else in the
  /// application can proceed until the attmpted error recovery
  /// completes.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool
}

extension RecoverableError {
  /// By default, implements document-modal recovery via application-model
  /// recovery.
  func attemptRecovery(optionIndex recoveryOptionIndex: Int,
                       andThen resultHandler: (Bool) -> Void) {
    resultHandler(recovered: attemptRecovery(optionIndex: recoveryOptionIndex))
  }
}
Error types that conform to RecoverableError may be given an opportunity to recover from the error. The user can be presented with some number of (localized) recovery options, described by recoveryOptions, and the selected option will be passed to the appropriate attemptRecovery method.

The CustomNSError protocol describes an error that wants to provide custom NSError information. This can be used, e.g., to provide a specific domain/code or to populate NSError's userInfo dictionary with values for custom keys that can be accessed from Objective-C code but are not covered by the other protocols.

/// Describes an error type that fills in the userInfo directly.
protocol CustomNSError : Error {
  var errorDomain: String { get }
  var errorCode: Int { get }
  var errorUserInfo: [String : AnyObject] { get }
}
Note that, unlike with NSError, the provided errorUserInfo requires String keys. This is in line with common practice for NSError and is important for the implementation (see below). All of these properties are defaulted, so one can provide any subset:

extension CustomNSError {
  var errorDomain: String { ... }
  var errorCode: Int { ... }
  var errorUserInfo: [String : AnyObject] { ... }
}
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#mapping-error-types-to-nserror>Mapping error types to NSError

Every type that conforms to the Error protocol is implicitly bridged to NSError. This has been the case since Swift 2, where the compiler provides a domain (i.e., the mangled name of the type) and code (based on the discriminator of the enumeration type). This proposal also allows for the userInfo dictionary to be populated by the runtime, which will check for conformance to the various protocols (LocalizedError, RecoverableError, or CustomNSError) to retrieve information.

Conceptually, this could be implemented by eagerly creating a userInfo dictionary for a given instance of Error:

func createUserInfo(error: Error) -> [NSObject : AnyObject] {
  var userInfo: [NSObject : AnyObject] = [:]

  // Retrieve custom userInfo information.
  if let customUserInfoError = error as? CustomNSError {
    userInfo = customUserInfoError.userInfo
  }

  if let localizedError = error as? LocalizedError {
    if let description = localizedError.errorDescription {
      userInfo[NSLocalizedDescriptionKey] = description
    }

    if let reason = localizedError.failureReason {
      userInfo[NSLocalizedFailureReasonErrorKey] = reason
    }

    if let suggestion = localizedError.recoverySuggestion {
      userInfo[NSLocalizedRecoverySuggestionErrorKey] = suggestion
    }

    if let helpAnchor = localizedError.helpAnchor {
      userInfo[NSHelpAnchorErrorKey] = helpAnchor
    }
  }

  if let recoverableError = error as? RecoverableError {
    userInfo[NSLocalizedRecoveryOptionsErrorKey] = recoverableError.recoveryOptions
    userInfo[NSRecoveryAttempterErrorKey] = RecoveryAttempter()
  }
}
The RecoveryAttempter class is an implementation detail. It will implement the informal protocol NSErrorRecoveryAttempting <https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Protocols/NSErrorRecoveryAttempting_Protocol/> for the given error:

class RecoveryAttempter : NSObject {
  @objc(attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo:)
  func attemptRecovery(fromError nsError: NSError,
                       optionIndex recoveryOptionIndex: Int,
                       delegate: AnyObject?,
                       didRecoverSelector: Selector,
                       contextInfo: UnsafeMutablePointer<Void>) {
    let error = nsError as! RecoverableError
    error.attemptRecovery(optionIndex: recoveryOptionIndex) { success in
      // Exposition only: this part will actually have to be
      // implemented in Objective-C to pass the BOOL and void* through.
      delegate?.perform(didRecoverSelector, with: success, with: contextInfo)
    }
  }

  @objc(attemptRecoveryFromError:optionIndex:)
  func attemptRecovery(fromError nsError: NSError,
                       optionIndex recoveryOptionIndex: Int) -> Bool {
    let error = nsError as! RecoverableError
    return error.attemptRecovery(optionIndex: recoveryOptionIndex)
  }
}
The actual the population of the userInfo dictionary should not be eager. NSError provides the notion of global "user info value providers" that it uses to lazily request the values for certain keys, via setUserInfoValueProvider(forDomain:provider:), which is declared as:

extension NSError {
  @available(OSX 10.11, iOS 9.0, tvOS 9.0, watchOS 2.0, *)
  class func setUserInfoValueProvider(forDomain errorDomain: String,
                                      provider: ((NSError, String) -> AnyObject?)? = nil)
}
The runtime would need to register a user info value provider for each error type the first time it is bridged into NSError, supplying the domain and the following user info value provider function:

func userInfoValueProvider(nsError: NSError, key: String) -> AnyObject? {
  let error = nsError as! Error
  switch key {
  case NSLocalizedDescriptionKey:
    return (error as? LocalizedError)?.errorDescription

  case NSLocalizedFailureReasonErrorKey:
    return (error as? LocalizedError)?.failureReason

  case NSLocalizedRecoverySuggestionErrorKey:
    return (error as? LocalizedError)?.recoverySuggestion

  case NSHelpAnchorErrorKey:
    return (error as? LocalizedError)?.helpAnchor

  case NSLocalizedRecoveryOptionsErrorKey:
    return (error as? RecoverableError)?.recoveryOptions

  case NSRecoveryAttempterErrorKey:
    return error is RecoverableError ? RecoveryAttempter() : nil

  default:
    guard let customUserInfoError = error as? CustomNSError else { return nil }
    return customUserInfoError.userInfo[key]
  }
}
On platforms that predate the introduction of user info value providers, there are alternate implementation strategies, including introducing a custom NSDictionary subclass to use as the userInfo in the NSError that lazily populates the dictionary by, effectively, calling the userInfoValueProvider function above for each requested key. Or, one could eagerly populate userInfo on older platforms.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#importing-error-types-from-objective-c>Importing error types from Objective-C

In Objective-C, error domains are typically constructed using an enumeration describing the error codes and a constant describing the error domain, e.g,

extern NSString *const AVFoundationErrorDomain;

typedef NS_ENUM(NSInteger, AVError) {
  AVErrorUnknown = -11800,
  AVErrorOutOfMemory = -11801,
  AVErrorSessionNotRunning = -11803,
  AVErrorDeviceAlreadyUsedByAnotherSession = -11804,
  // ...
}
This is currently imported as an enum that conforms to Error:

enum AVError : Int {
  case unknown = -11800
  case outOfMemory = -11801
  case sessionNotRunning = -11803
  case deviceAlreadyUsedByAnotherSession = -11804

  static var _domain: String { return AVFoundationErrorDomain }
}
and Swift code introduces an extension that makes it an Error, along with some implementation magic to allow bridging from an NSError (losing userInfo in the process):

extension AVError : Error {
  static var _domain: String { return AVFoundationErrorDomain }
}
Instead, error enums should be expressed with a new macro, NS_ERROR_ENUM, that ties together the code and domain in the Objective-C header:

extern NSString *const AVFoundationErrorDomain;

typedef NS_ERROR_ENUM(NSInteger, AVError, AVFoundationErrorDomain) {
  AVErrorUnknown = -11800,
  AVErrorOutOfMemory = -11801,
  AVErrorSessionNotRunning = -11803,
  AVErrorDeviceAlreadyUsedByAnotherSession = -11804,
  // ...
}
This will import as a new struct AVError that contains an NSError, so there is no information loss. The actual enum will become a nested type Code, so that it is still accessible. The resulting struct will be as follows:

struct AVError {
  /// Stored NSError. Note that error.domain == AVFoundationErrorDomain is an invariant.
  private var error: NSError

  /// Describes the error codes; directly imported from AVError
  enum Code : Int, ErrorCodeProtocol {
    typealias ErrorType = AVError

    case unknown = -11800
    case outOfMemory = -11801
    case sessionNotRunning = -11803
    case deviceAlreadyUsedByAnotherSession = -11804

    func errorMatchesCode(_ error: AVError) -> Bool {
      return error.code == self
    }
  }

  /// Allow one to create an error (optionally) with a userInfo dictionary.
  init(_ code: Code, userInfo: [NSObject: AnyObject] = [:]) {
    error = NSError(code: code.rawValue, domain: _domain, userInfo: userInfo)
  }

  /// Retrieve the code.
  var code: Code { return Code(rawValue: error.code)! }

  /// Allow direct access to the userInfo dictionary.
  var userInfo: [NSObject: AnyObject] { return error.userInfo }

  /// Make it easy to refer to constants without context.
  static let unknown: Code = .unknown
  static let outOfMemory: Code = .outOfMemory
  static let sessionNotRunning: Code = .sessionNotRunning
  static let deviceAlreadyUsedByAnotherSession: Code = .deviceAlreadyUsedByAnotherSession
}

// Implementation detail: makes AVError conform to Error
extension AVError : Error {
  static var _domain: String { return AVFoundationErrorDomain }

  var _code: Int { return error.code }
}
This syntax allows one to throw specific errors fairly easily, with or without userInfo dictionaries:

throw AVError(.sessionNotRunning)
throw AVError(.sessionNotRunning, userInfo: [ ... ])
The ImportedErrorCode protocol is a helper so that we can define a general ~= operator, which is used by both switchcase matching and catch blocks:

protocol ErrorCodeProtocol {
  typealias ErrorType : Error

  func errorMatchesCode(_ error: ErrorType) -> Bool
}

func ~= <EC: ErrorCodeProtocol> (error: Error, code: EC) -> Bool {
  guard let myError = error as? EC.ErrorType else { return false }
  return code.errorMatchesCode(myError)
}
<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#mapping-nserror-types-back-into-swift>Mapping NSError types back into Swift

When an NSError object bridged to an Error instance, it may be immediately mapped back to a Swift error type (e.g., if the error was created as a HomeworkError instance in Swift and then passed through NSError unmodified) or it might be leave as an instance of NSError. The error might then be catch as a particular Swift error type, e.g.,

catch let error as AVError where error.code == .sessionNotRunning {
  // able to access userInfo here!
}
In this case, the mapping from an NSError instance to AVError goes through an implementation-detail protocol_ObjectiveCBridgeableError:

protocol _ObjectiveCBridgeableError : Error {
  /// Produce a value of the error type corresponding to the given NSError,
  /// or return nil if it cannot be bridged.
  init?(_bridgedNSError error: NSError)
}
The initializer is responsible for checking the domain and (optionally) the code of the incoming NSError to map it to an instance of the Swift error type. For example, AVError would adopt this protocol as follows:

// Implementation detail: makes AVError conform to _ObjectiveCBridgeableError
extension AVError : _ObjectiveCBridgeableError {
  init?(_bridgedNSError error: NSError) {
    // Check whether the error comes from the AVFoundation error domain
    if error.domain != AVFoundationErrorDomain { return nil }

    // Save the error
    self.error = error
  }
}
We do not propose that _ObjectiveCBridgeableError become a public protocol, because the core team has already deferred a similar proposal (SE-0058 <https://github.com/apple/swift-evolution/blob/master/proposals/0058-objectivecbridgeable.md>) to make the related protocol _ObjectiveCBridgeable public.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#other-issues>Other Issues

NSError codes and domains are important for localization of error messages. This is barely supported today by genstrings, but becomes considerably harder when the domain and code are hidden (as they are in Swift). We would need to consider tooling to make it easier to localize error descriptions, recovery options, etc. in a sensible way. Although this is out of the scope of the Swift language per se, it's an important part of the developer story.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#impact-on-existing-code>Impact on existing code

This is a major source-breaking change for Objective-C APIs that operate on NSError values, because those parameter/return/property types will change from NSError to Error. There are ~400 such APIs in the macOS SDK, and closer to 500 in the iOS SDK, which is a sizable number. Fortunately, this is similar in scope to the Foundation value types proposal <https://github.com/apple/swift-evolution/blob/master/proposals/0069-swift-mutability-for-foundation.md>, and can use the same code migration mechanism. That said, the scale of this change means that it should either happen in Swift 3 or not at all.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#future-directions>Future directions

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#better-tooling-for-describing-errors>Better tooling for describing errors

When adopting one of the new protocols (e.g., LocalizedError) in an enum, one will inevitably end up with a number of switch statements that have to enumerate all of the cases, leading to a lot of boilerplate. Better tooling could improve the situation considerably: for example, one could use something like Cocoa's stringsdict files <https://developer.apple.com/library/prerelease/content/documentation/MacOSX/Conceptual/BPInternational/StringsdictFileFormat/StringsdictFileFormat.html> to provide localized strings identified by the enum name, case name, and property. That would eliminate the need for the switch-on-all-cases implementations of each property.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#round-tripping-errors-through-userinfo>Round-tripping errors through userInfo

The CustomNSError protocol allows one to place arbitrary key/value pairs into NSError's userInfo dictionary. The implementation-detail _ObjectiveCBridgeableError protocol allows one to control how a raw NSError is mapped to a particular error type. One could effectively serialize the entire state of a particular error type into the userInfo dictionary viaCustomNSError, then restore it via _ObjectiveCBridgeableError, allowing one to form a complete NSError in Objective-C that can reconstitute itself as a particular Swift error type, which can be useful both for mixed-source projects and (possibly) as a weak form of serialization for NSErrors.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#alternatives-considered>Alternatives considered

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#exposing-the-domain-code-and-user-info-dictionary-directly>Exposing the domain, code, and user-info dictionary directly

This proposal does not directly expose the domain, code, or user-info dictionary on ErrorProtocol, because these notions are superseded by Swift's strong typing of errors. The domain is effectively subsumed by the type of the error (e.g., a Swift-defined error type uses its mangled name as the domain); the code is some type-specific value (e.g., the discriminator of the enum); and the user-info dictionary is an untyped set of key-value pairs that are better expressed in Swift as data on the specific error type.

<https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#bridging-nserror-to-a-new-value-type-error>Bridging NSError to a new value type Error

One could introduce a new value type, Error, that stores a domain, code, and user-info dictionary but provides them with value semantics. Doing so would make it easier to create "generic" errors that carry some information. However, we feel that introducing new error types in Swift is already easier than establishing a new domain and a set of codes, because a new enum type provides this information naturally in Swift.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Douglas Gregor) #9

I’m floating CustomNSError as the protocol name because I don’t feel that domain, core, or userInfo are fundamental to the Swift error model—they’re about exposing things specifically to NSError.

  - Doug

···

On Jun 27, 2016, at 1:58 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

Obviously, I’m in favor of this one. +1!

I think I did prefer the older name of CustomUserInfoError for the domain/code/userInfo protocol, rather than CustomNSError. This is just because I’d like to be able to do a global search through my project for “NSError” and have it turn up empty. Maybe a silly reason, I know. :wink:


(Charles Srstka) #10

I'm not sure I really want '.url' and '.stringEncoding' on every
Error. 'var underlying' is universally useful, but providing it
requires a implementing conformance to CustomNSError, which has to
vend a weakly-typed dictionary. Is this really the API we want to
expose?

We need to expose the dictionary in order to provide full compatibility with NSError.

Also, the underlying error has to be stored somewhere, which
effectively prevents implementers of CustomNSError from being enums.

Not at all. Your enum can implement a dynamic errorUserInfo property that will populate the dictionary with the appropriate values. If you need to actually store something, that can be done with enum cases as well.

Charles

···

On Jun 29, 2016, at 2:50 AM, Dmitri Gribenko via swift-evolution <swift-evolution@swift.org> wrote:


(Charles Srstka) #11

The trouble is that NSError allows NSLocalizedDescriptionKey to be nil, and leaving it nil is how you get Cocoa's default behavior in a lot of situations. In fact, this is usually what you want—leaving NSLocalizedDescriptionKey nil and populating NSLocalizedFailureReasonErrorKey instead is often the better way to go. For example, in NSDocument’s error reporting, if you throw an error that sets a failure reason, like this:

override func read(from data: Data, ofType typeName: String) throws {
    let userInfo = [NSLocalizedFailureReasonErrorKey: "Something went wrong."]
    throw NSError(domain: "Foo", code: 1, userInfo: userInfo)
}

The error is presented to the user as “The operation could not be completed. Something went wrong.”

However, if you provide the description instead:

override func read(from data: Data, ofType typeName: String) throws {
    let userInfo = [NSLocalizedDescriptionKey: "Something went wrong."]
    throw NSError(domain: "Foo", code: 1, userInfo: userInfo)
}

You just get “The operation could not be completed.” with no further information.

Providing the failure reason while leaving the description nil also changes the presentation when you’re reporting errors directly, as below:

let userInfo = [NSLocalizedFailureReasonErrorKey: "Something went wrong."]
NSApp.presentError(NSError(domain: "Foo", code: 1, userInfo: userInfo))

This gives you “The operation could not be completed. Something went wrong.” By comparison:

let userInfo = [NSLocalizedDescriptionKey: "Something went wrong."]
NSApp.presentError(NSError(domain: "Foo", code: 1, userInfo: userInfo))

This just gives you “Something went wrong.” without the polite “The operation could not be completed.” prefix, which causes the error description to come across as rather blunt. A default implementation for the property could be provided, of course, but since the primary purpose of the methods in this protocol are for the implementer of the error type to provide information to the frameworks to assist in creating the NSError, I can’t think of any way to have it simultaneously return a meaningful value to a client and still communicate to the frameworks that this value should be nil.

Perhaps we should add an additional property for a user-facing string generated from the rest of the strings?

Charles

···

On Jun 29, 2016, at 5:30 PM, Paul Cantrell via swift-evolution <swift-evolution@swift.org> wrote:

On Jun 27, 2016, at 1:17 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

The LocalizedError protocol describes an error that provides localized messages for display to the end user, all of which provide default implementations. The conforming type can provide implementations for any subset of these requirements:

protocol LocalizedError : Error {
  /// A localized message describing what error occurred.
  var errorDescription: String? { get }
  …
}

Given that LocalizedError would now be its own protocol that not all errors would conform to, could errorDescription be non-optional?

  var errorDescription: String { get }

It would be nice if conformance to LocalizedError guaranteed the presence of a user-readable message. Such a guarantee is useful when building a UI.

I realize the bridging to NSError may make this impossible, but in principle it seems like the right design.


(Douglas Gregor) #12

Maybe. We had originally tried doing this, but it caused a lot of frustration because NSError’s userInfo historically allowed non-string keys, so technically it should be typed as [NSObject : AnyObject]. However, that type signature means that one is “.rawValue”’ing all of the keys over and over:

    userInfo[NSErrorUserInfoKey.myKeyName.rawValue] = foo

That said, this proposal eliminates the need for using those keys (and, indeed, NSError) most of the time, and maybe we can write off non-string keys entirely in Swift.

  - Doug

···

On Jun 30, 2016, at 2:19 AM, Brent Royal-Gordon <brent@architechies.com> wrote:

Note that, unlike with NSError, the provided errorUserInfo requires String keys.

Is there any way this could be tightened further to require Error.UserInfoKey keys (where Error.UserInfoKey is a Notification.Name-style wrapper)?


(Riley Testut) #13

Love the proposal overall, but not sure about the CustomNSError name either. It doesn’t seem to read like a Swift protocol name.

Somewhat related, is there a reason these protocols don’t contain the “Protocol” suffix? Stands in stark contrast with the rest of the Swift protocol naming conventions (AFAIK).

···

On Jun 28, 2016, at 4:33 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

On Jun 27, 2016, at 1:58 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

Obviously, I’m in favor of this one. +1!

I think I did prefer the older name of CustomUserInfoError for the domain/code/userInfo protocol, rather than CustomNSError. This is just because I’d like to be able to do a global search through my project for “NSError” and have it turn up empty. Maybe a silly reason, I know. ;-)

I’m floating CustomNSError as the protocol name because I don’t feel that domain, core, or userInfo are fundamental to the Swift error model—they’re about exposing things specifically to NSError.

  - Doug

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dmitri Gribenko) #14

I'm not sure I really want '.url' and '.stringEncoding' on every
Error. 'var underlying' is universally useful, but providing it
requires a implementing conformance to CustomNSError, which has to
vend a weakly-typed dictionary. Is this really the API we want to
expose?

We need to expose the dictionary in order to provide full compatibility with
NSError.

The full compatibility argument is universal, and it can be applied to
anything. Not always the answer is "dump the compatibility APIs onto
the Swift type".

Also, the underlying error has to be stored somewhere, which
effectively prevents implementers of CustomNSError from being enums.

Not at all. Your enum can implement a dynamic errorUserInfo property that
will populate the dictionary with the appropriate values. If you need to
actually store something, that can be done with enum cases as well.

You would need to store the underlying error in every enum case, which
creates boilerplate, and you'd lose the raw representable conformance.

Dmitri

···

On Wed, Jun 29, 2016 at 4:13 AM, Charles Srstka <cocoadev@charlessoft.com> wrote:

On Jun 29, 2016, at 2:50 AM, Dmitri Gribenko via swift-evolution > <swift-evolution@swift.org> wrote:

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/


(Douglas Gregor) #15

Love the proposal overall, but not sure about the CustomNSError name either. It doesn’t seem to read like a Swift protocol name.

Somewhat related, is there a reason these protocols don’t contain the “Protocol” suffix? Stands in stark contrast with the rest of the Swift protocol naming conventions (AFAIK).

Protocols don’t always have the suffix “Protocol”; it’s used when there might be confusion. The API Design Guidelines have this to say about protocols:

Protocols that describe what something is should read as nouns (e.g. Collection).

Protocols that describe a capability should be named using the suffixes able, ible, or ing (e.g. Equatable, ProgressReporting).

I think all of the names in the proposal fit into that first bullet.

  - Doug

···

On Jun 28, 2016, at 10:31 PM, Riley Testut <rileytestut@gmail.com> wrote:

On Jun 28, 2016, at 4:33 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

On Jun 27, 2016, at 1:58 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

Obviously, I’m in favor of this one. +1!

I think I did prefer the older name of CustomUserInfoError for the domain/code/userInfo protocol, rather than CustomNSError. This is just because I’d like to be able to do a global search through my project for “NSError” and have it turn up empty. Maybe a silly reason, I know. :wink:

I’m floating CustomNSError as the protocol name because I don’t feel that domain, core, or userInfo are fundamental to the Swift error model—they’re about exposing things specifically to NSError.

  - Doug

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Charlie Monroe) #16

Perhaps it could be CustomCocoaError, having the "Cocoa" part precedence in

String(cocoaString: NSString)

···

On Jun 29, 2016, at 7:31 AM, Riley Testut via swift-evolution <swift-evolution@swift.org> wrote:

Love the proposal overall, but not sure about the CustomNSError name either. It doesn’t seem to read like a Swift protocol name.

Somewhat related, is there a reason these protocols don’t contain the “Protocol” suffix? Stands in stark contrast with the rest of the Swift protocol naming conventions (AFAIK).

On Jun 28, 2016, at 4:33 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

On Jun 27, 2016, at 1:58 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

Obviously, I’m in favor of this one. +1!

I think I did prefer the older name of CustomUserInfoError for the domain/code/userInfo protocol, rather than CustomNSError. This is just because I’d like to be able to do a global search through my project for “NSError” and have it turn up empty. Maybe a silly reason, I know. :wink:

I’m floating CustomNSError as the protocol name because I don’t feel that domain, core, or userInfo are fundamental to the Swift error model—they’re about exposing things specifically to NSError.

  - Doug

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Charles Srstka) #17

I'm not sure I really want '.url' and '.stringEncoding' on every
Error. 'var underlying' is universally useful, but providing it
requires a implementing conformance to CustomNSError, which has to
vend a weakly-typed dictionary. Is this really the API we want to
expose?

We need to expose the dictionary in order to provide full compatibility with
NSError.

The full compatibility argument is universal, and it can be applied to
anything. Not always the answer is "dump the compatibility APIs onto
the Swift type”.

In this case, the type in question is the error type returned by all of the error-returning APIs in the frameworks, as well as the vast library of Objective-C code out there in the community. I think that being able to access all the information provided by said error type is a fairly important goal.

Also, the APIs are not “dumped onto the Swift type.” They are added in an extension in Foundation, and if you don’t import Foundation, you won’t see them. This is consistent with the way other bridged types are treated; for example, Foundation has an extension on String that provides most of the goodies provided by NSString. This allows us to write code using Strings exclusively, with NSString rarely, if ever, actually needing to appear in Swift code, which is what we want to achieve with NSError->Error.

Also, the underlying error has to be stored somewhere, which
effectively prevents implementers of CustomNSError from being enums.

Not at all. Your enum can implement a dynamic errorUserInfo property that
will populate the dictionary with the appropriate values. If you need to
actually store something, that can be done with enum cases as well.

You would need to store the underlying error in every enum case, which
creates boilerplate, and you'd lose the raw representable conformance.

Only on the cases for which an underlying error is relevant, which may only be a few cases. Alternatively, you could use a struct if that fits your design better, or you could go with a hybrid approach such as an enum inside a struct, or if RawRepresentable conformance is important, you could forego providing an underlying error. Simply use the type of error that most appropriately fits your project and the conceptual type of error you wish to report.

If you do decide to provide an underlying error, you will need to create a little boilerplate, yes, whether it’s in an enum case, a struct’s initializer, or somewhere else. I don’t think that is avoidable in any case. I will still contend that it is much less boilerplate than having to fill your code with this:

let userInfo = [NSLocalizedFailureReasonErrorKey: NSLocalizedString(“A horse, a horse, my kingdom for a horse!”, comment: “Tudors are in, Yorks are out"), NSRecoverySuggestionErrorKey: NSLocalizedString(“Time for a new king.”, comment: “The Henry that doesn’t have a Shakespeare play”), NSFilePathErrorKey: “/Europe/England/Leicestershire/Bosworth”, NSURLErrorKey: NSURL(string: "https://en.wikipedia.org/wiki/Battle_of_Bosworth_Field”), NSUnderlyingErrorKey: wantedAHorseshoeNail]
throw NSError(domain: “OhMyGodWhyDoINeedToTypeAllThis”, code: WarOfRosesError.HorseDied.rawValue, userInfo: userInfo)

and then, when you need to check for the thing later on, you have to:

do {
    try something()
} catch let error as NSError {
    if error.domain == “OhMyGodWhyDoINeedToTypeAllThis” && error.code == WarOfRosesError.HorseDied.rawValue {
        self.setUpHenryVII()
    }
} catch {
    // handle normal errors
}

Yeah, I think we win overall in the boilerplate department by just turning that into a enum case or a struct with an “underlying” argument on it. :stuck_out_tongue:

Charles

···

On Jun 29, 2016, at 12:05 PM, Dmitri Gribenko <gribozavr@gmail.com> wrote:
On Wed, Jun 29, 2016 at 4:13 AM, Charles Srstka > <cocoadev@charlessoft.com> wrote:

On Jun 29, 2016, at 2:50 AM, Dmitri Gribenko via swift-evolution >> <swift-evolution@swift.org> wrote:


(Douglas Gregor) #18

I’m not sure I really want ‘.url’ and ‘.stringEncoding’ on every
Error. ‘var underlying’ is universally useful, but providing it
requires a implementing conformance to CustomNSError, which has to
vend a weakly-typed dictionary. Is this really the API we want to
expose?

We need to expose the dictionary in order to provide full compatibility with
NSError.

The full compatibility argument is universal, and it can be applied to
anything. Not always the answer is "dump the compatibility APIs onto
the Swift type”.

Yes, that’s a reasonable point. These APIs are available by bouncing through NSError and (at worst) looking up a known key in its userInfo dictionary. I only truly care about localizedDescription being available on Error(Protocol), because that’s universal and useful. The others… I think I’ll go put them on (NS)CocoaError, which covers the Cocoa error domain and is where one might reasonably expect these keys to show up. It’s fairly easy for other, specific error types to do the same if they need them. If we truly need some of these on all error types, we can add that at a later time.

I’ll revise the proposal shortly.

Also, the underlying error has to be stored somewhere, which
effectively prevents implementers of CustomNSError from being enums.

Not at all. Your enum can implement a dynamic errorUserInfo property that
will populate the dictionary with the appropriate values. If you need to
actually store something, that can be done with enum cases as well.

You would need to store the underlying error in every enum case, which
creates boilerplate, and you’d lose the raw representable conformance.

If it’s important for the error type to store the underlying error, you can use a struct or add the payload to every enum case. Maybe some future feature will make the latter option easier (I feel like we saw a pitch about that before).

- Doug
···

On Jun 29, 2016, at 10:05 AM, Dmitri Gribenko gribozavr@gmail.com wrote:
On Wed, Jun 29, 2016 at 4:13 AM, Charles Srstka > cocoadev@charlessoft.com wrote:

On Jun 29, 2016, at 2:50 AM, Dmitri Gribenko via swift-evolution > > swift-evolution@swift.org wrote:


(Michael Peternell) #19

Hmm, not all NSErrors are Cocoa Errors. CustomNSError is a perfectly scoped name: CustomError would be too vague, CustomCocoaError excludes errors thrown by AVFoundation. And the intent is perfectly clear without explanation. I think a CustomCocoaError would have documentation comment that says that it can be used to customize "NSError behavior". After all, a `CustomNSError` is a custom `NSError`.

-Michael

···

Am 29.06.2016 um 08:45 schrieb Charlie Monroe via swift-evolution <swift-evolution@swift.org>:

Perhaps it could be CustomCocoaError, having the "Cocoa" part precedence in

String(cocoaString: NSString)

On Jun 29, 2016, at 7:31 AM, Riley Testut via swift-evolution <swift-evolution@swift.org> wrote:

Love the proposal overall, but not sure about the CustomNSError name either. It doesn’t seem to read like a Swift protocol name.

Somewhat related, is there a reason these protocols don’t contain the “Protocol” suffix? Stands in stark contrast with the rest of the Swift protocol naming conventions (AFAIK).

On Jun 28, 2016, at 4:33 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

On Jun 27, 2016, at 1:58 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

Obviously, I’m in favor of this one. +1!

I think I did prefer the older name of CustomUserInfoError for the domain/code/userInfo protocol, rather than CustomNSError. This is just because I’d like to be able to do a global search through my project for “NSError” and have it turn up empty. Maybe a silly reason, I know. :wink:

I’m floating CustomNSError as the protocol name because I don’t feel that domain, core, or userInfo are fundamental to the Swift error model—they’re about exposing things specifically to NSError.

  - Doug

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #20

I'm not sure I really want '.url' and '.stringEncoding' on every
Error. 'var underlying' is universally useful, but providing it
requires a implementing conformance to CustomNSError, which has to
vend a weakly-typed dictionary. Is this really the API we want to
expose?

We need to expose the dictionary in order to provide full compatibility with
NSError.

The full compatibility argument is universal, and it can be applied to
anything. Not always the answer is "dump the compatibility APIs onto
the Swift type”.

Yes, that’s a reasonable point. These APIs are available by bouncing through NSError and (at worst) looking up a known key in its userInfo dictionary. I only truly care about localizedDescription being available on Error(Protocol), because that’s universal and useful. The others… I think I’ll go put them on (NS)CocoaError, which covers the Cocoa error domain and is where one might reasonably expect these keys to show up. It’s fairly easy for other, specific error types to do the same if they need them. If we truly need some of these on all error types, we can add that at a later time.

I’ll revise the proposal shortly.

Also, the underlying error has to be stored somewhere, which
effectively prevents implementers of CustomNSError from being enums.

Not at all. Your enum can implement a dynamic errorUserInfo property that
will populate the dictionary with the appropriate values. If you need to
actually store something, that can be done with enum cases as well.

You would need to store the underlying error in every enum case, which
creates boilerplate, and you'd lose the raw representable conformance.

If it’s important for the error type to store the underlying error, you can use a struct or add the payload to every enum case. Maybe some future feature will make the latter option easier (I feel like we saw a pitch about that before).

There was a pitch along these lines. IIRC it was something like stored properties for enums where you could declare the property once and it would be added to the payload of all cases as well as available directly as a property. I thought it sounded useful - underlying error is an example of a good use case.

I'm pretty sure there was pretty strong negative feedback along the lines that is blurs the distinction between struct and enum too much and I think that feedback came from someone on the core team. I don't recall the exact details though.

···

Sent from my iPad

On Jun 30, 2016, at 12:56 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

On Jun 29, 2016, at 10:05 AM, Dmitri Gribenko <gribozavr@gmail.com> wrote:
On Wed, Jun 29, 2016 at 4:13 AM, Charles Srstka >> <cocoadev@charlessoft.com> wrote:

On Jun 29, 2016, at 2:50 AM, Dmitri Gribenko via swift-evolution >>> <swift-evolution@swift.org> wrote:

  - Doug

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution