Apple provides the following example in their documentation:
do {
try customThrow()
} catch MyError.specificError1 {
print("Caught specific error #1")
} catch let error as MyError where error.code == .specificError2 {
print("Caught specific error #2, ", error.localizedDescription)
// Prints "Caught specific error #2. A customized error from MyErrorDomain."
} catch let error {
fatalError("Some other error: \(error)")
}
Why does this line work:
} catch let error as MyError where error.code == .specificError2 {
yet the following line results in nil
:
let e = error as? MyError
?
Either error
can be casted to MyError
or it cannot, correct? So how can these two lines produce different results? Does the behavior of as
varies on context and is different in catch
clauses?
The linked documentation is about Cocoa errors, that means NSError
objects. So to reproduce, you'd do something along these lines:
extern NSErrorDomain const MyErrorDomain;
typedef NS_ERROR_ENUM(MyErrorDomain, MyError) {
specificError1 = 0,
specificError2 = 1
};
then you create and pass the error in Obj-C, not in Swift:
NSError * error = [NSError errorWithDomain:MyErrorDomain
code:specificError1 userInfo:nil];
[someObj handleError:error];
and in Swift you have:
func handleError ( error: NSError ) {
if let e = error as? MyError {
// Never happens, never true
}
}
Yet when thrown as an error:
- (BOOL)throwError:(NSError **)outError {
*outError = [NSError errorWithDomain:MyErrorDomain
code:specificError1 userInfo:nil];
return NO;
}
and then caught in Swift, this is supposed to work correctly according to Apple's documentation. And I wonder how this can be.
Following the code path, the error was once passed along as CFErrorRef
, which is possible thank to toll-free bridging but when the error is converted back to a NSError
, it won't look the same to Swift anymore.
This is the Swift debugger output when the error was originally created:
(lldb) p error
(NSError) $R0 = 0x0000000100606ad0 domain: "MyErrorDomain" - code: 0 {
_userInfo = 0x00007fff80983090
}
And this is the output after it traveled around as a CFErrorRef
and becomes a NSError
again:
(lldb) p error
(NSError) $R1 = 0x0000000105505360 domain: "MyErrorDomain" - code: 0 {
ObjectiveC.NSObject = {
baseNSObject@0 = {
isa = __SwiftNativeNSError
}
_reserved = 0x0000000000000000
_code = 0
_domain = 0x0000000100004230 "MyErrorDomain"
_userInfo = 0x00007fff80983090
}
}
As you can see, it's the same error. Same domain, same code but now it's not castable anymore to MyError
.
Okay, the problem is that Apple does some kind of magic with NSError
that is lost along the following way:
ObjC -> C -> Swift -> ObjC -> Swift
Apparently not all NSError
are the same to Swift, even though they are to Obj-C.
To pass that error through C, it must be a CFErrorRef
. To get the error back into Obj-C, it must be a NSError
again, but the Swift Code retrieves the error as Unmanaged<CFError>?
.
The code that converted the error back did the following:
if let unmanagedError = errorRaw {
let error = unmanagedError.takeRetainedValue() as Error as NSError
if error.domain == "..." {
Only for specific domains it needs to forward the code Obj-C, hence the double cast, as casting directly to NSError
is not allowed. The problem is now, if that error is finally given back to Swift code, it cannot be casted to a Swift error any longer, despite having the correct domain and code of such an error.
The actual fix that works is to modify the code above as follows:
let cfe = unmanagedError.takeRetainedValue()
let nse = cfe as Error as NSError
let error = NSError(domain: nse.domain,
code: nse.code, userInfo: nse.userInfo);
if error.domain == "..." {
As stupid as this code may seem, it does make a difference to Swift whether the error was casted or re-created, while the Obj-C in between simply won't care as for that code it makes no difference at all.
The behavior of as
does not depend on context but it behaves on more than just types. Unlike a cast in C/Obj-C, the rule that "you either can always cast type A to type B or never do so" does not apply to Swift, since "as" dynamic casting and if a cast is possible at runtime and not can depend on more than just source and destination types.
In case of NSError
it also depends on property values (domain
and code
) and also on the internal representation of the object. While the first fact makes sense, second one does not and IMHO is an implementation quirk of the Swift runtime, as when comparing those two different errors above using isEqual:
, then they actually are and for equal objects, the same cast rules should apply as equality means these objects are "interchangeable", which they are apparently not if one of them can be successfully casted and the other one not.