21

A lot of times, I'll receive a Swift Error object from a framework, which is really an NSError.

In order to access its information (e.g. code), I need to cast it to an NSError:

(error as NSError).code == ....

Why is this just an unconditional as? If I design my own error class that conforms to Error, it won't necessarily be an NSError, so how can this be the correct way to perform this cast?

Is there some kind of special case in the type system? This is a downcast that behaves like an upcast.

Bill
  • 44,502
  • 24
  • 122
  • 213

3 Answers3

18

I believe the capability for Error to be convertible to NSError is hardcoded into the compiler, and the actual bridging is implemented in the Swift runtime.

In runtime/ErrorObject.mm, I found this comment:

// This implements the object representation of the standard Error
// type, which represents recoverable errors in the language. This
// implementation is designed to interoperate efficiently with Cocoa libraries
// by:
// - ...
// - allowing a native Swift error to lazily "become" an NSError when
//   passed into Cocoa, allowing for cheap Swift to Cocoa interop

And this function:

/// Take an Error box and turn it into a valid NSError instance.
id
swift::_swift_stdlib_bridgeErrorToNSError(SwiftError *errorObject) {
    ...

  // Otherwise, calculate the domain, code, and user info, and
  // initialize the NSError.
  auto value = SwiftError::getIndirectValue(&errorObject);
  auto type = errorObject->getType();
  auto witness = errorObject->getErrorConformance();

  NSString *domain = getErrorDomainNSString(value, type, witness);
  NSInteger code = getErrorCode(value, type, witness);
  NSDictionary *userInfo = getErrorUserInfoNSDictionary(value, type, witness);

  ...
}

The ErrorHandling.rst document says this about the rationale:

It should be possible to turn an arbitrary Swift enum that conforms to Error into an NSError by using the qualified type name as the domain key, the enumerator as the error code, and turning the payload into user data.

(Parts of the document may be outdated.)

And this is (I think) at least one part in the type checker were the information that Error is convertible to NSError is encoded (there are probably more):

  // Check whether the type is an existential that contains
  // Error. If so, it's bridged to NSError.
  if (type->isExistentialWithError()) {
    if (auto nsErrorDecl = getNSErrorDecl()) {
      // The corresponding value type is Error.
      if (bridgedValueType)
        *bridgedValueType = getErrorDecl()->getDeclaredInterfaceType();

      return nsErrorDecl->getDeclaredInterfaceType();
    }
  }
Ole Begemann
  • 135,006
  • 31
  • 278
  • 256
  • 1
    > the enumerator as the error code An enum which doesn't confirm to a native type like Int, how is the enumerator determined? Is it guaranteed to be in order the cases are listed? – Ayush Goel Nov 28 '19 at 10:46
4

This is a great question.

I thought I saw "An Error type can be bridged to an NSError" somewhere, but that must have been Xcode or some tutorial online.

Luckily I found this from swift/NSError.swift.

// NSError and CFError conform to the standard Error protocol. Compiler
// magic allows this to be done as a "toll-free" conversion when an NSError
// or CFError is used as an Error existential.
extension NSError : Error {
  @nonobjc
  public var _domain: String { return domain }

  @nonobjc
  public var _code: Int { return code }

  @nonobjc
  public var _userInfo: AnyObject? { return userInfo as NSDictionary }

  /// The "embedded" NSError is itself.
  @nonobjc
  public func _getEmbeddedNSError() -> AnyObject? {
    return self
  }
}

extension CFError : Error {
  public var _domain: String {
    return CFErrorGetDomain(self) as String
  }

  public var _code: Int {
    return CFErrorGetCode(self)
  }

  public var _userInfo: AnyObject? {
    return CFErrorCopyUserInfo(self) as AnyObject
  }

  /// The "embedded" NSError is itself.
  public func _getEmbeddedNSError() -> AnyObject? {
    return self
  }
}
funct7
  • 3,407
  • 2
  • 27
  • 33
  • 1
    Thanks for finding this! It still doesn't quite clear things up for me. It shows that both `NSError` and `CFError` implement `Error`, but it still doesn't show how I can take an arbitrary `Error` and make a 100%-safe cast to an `NSError` - it's like a downcast that behaves like an upcast. I guess the "compiler magic" part of the comment is where the solution lies. – Bill Jul 30 '18 at 23:28
  • True. There are other related sections, so hopefully that approximates to an answer :/ – funct7 Jul 30 '18 at 23:34
  • 1
    One other part of this that confuses me is that I can write a custom class that conforms to the `Error` protocol and I don't even need to provide the `localizedDescription` property in my class. Yet the class compiles and I can magically cast (using `as NSError`) my custom class to `NSError`. `Error` really seems to be a specially treated protocol. – rmaddy Jul 31 '18 at 00:39
-4
switch error {
case _ where (error as NSError).domain == self.domain:
   print("error from particular domain")
default:
   print("default error")
}
GameLoading
  • 6,688
  • 2
  • 33
  • 57