- Proposal: SE-0112
- Authors: Doug Gregor, Charles Srstka
- Review Manager: Chris Lattner
- Status: Implemented (Swift 3.0)
- Decision Notes: Rationale
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 to
ErrorProtocol
can be thrown and will be reflected as an
NSError
with a suitable domain and code. Moreover, an NSError
produced 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.
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
'suserInfo
in Swift. For example, the Swift-natural way to catch a specific error in theAVError
error domain doesn't give one access to theuserInfo
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 AVErrorTimeKey
will be typed
as CMTime
:
catch let error as NSError where error._domain = AVFoundationErrorDomain {
if let time = error.userInfo[AVErrorTimeKey] 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 withErrorProtocol
. Swift interoperates by translating betweenNSError
andErrorProtocol
when mapping between a throwing Swift method/initializer and an Objective-C method with anNSError**
parameter. However, an Objective-C method that takes anNSError*
parameter (e.g., to render it) does not bridge toErrorProtocol
, meaning thatNSError
is part of the API in Swift in some places (but not others). For example,NSError
leaks through when the followingUIDocument
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
.
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 toLocalizedError
:
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 anNSError
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
toErrorProtocol
, so that allNSError
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, one can still explicitly turn
an NSError
into ErrorProtocol
via a bridging cast, e.g.,
nsError as ErrorProtocol
.
- In Foundation, add an extension to
ErrorProtocol
that provides access to the localized description, which is available for all error types.
extension ErrorProtocol {
var localizedDescription: String {
return (self as! NSError).localizedDescription
}
}
For the Cocoa error domain, which is encapsulated by the
NSCocoaError
type, add typed access for 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 NSCocoaError {
// Note: for exposition only. Not actual API.
private var userInfo: [NSObject : AnyObject] {
return (self as! NSError).userInfo
}
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
toError
: once we've completed the bridging story,Error
becomes the primary way to work with error types in Swift, and the value type to whichNSError
is bridged:
func handleError(_ error: Error, userInteractionPermitted: Bool)
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
.
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 handler 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,
resultHandler handler: (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,
resultHandler handler: (recovered: Bool) -> Void) {
handler(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] { ... }
}
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.
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 switch
case
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)
}
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.
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.
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.
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.
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 via
CustomNSError
, 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
NSError
s.
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.
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.