Evaluating if Error is NSError always succeeds while casting Error as NSError gives a compiler error

2.1k Views Asked by At

We had an issue with an Error object that crashes in Crashlytics' Objective-c code because it doesn't respond to userInfo (a member of NSError), while investigating I stumbled upon this weird behavior in Swift.

In a playground, I tried creating a class SwiftError implementing the Error protocol:

class SwiftError: Error {
}

let sError = SwiftError()

if sError is NSError { // Generates a warning: 'is' test is always true
    print("Success")
} else {
    print("Fail")
}
// Prints Fail

let nsError = sError as NSError
//Compiler Error: 'SwiftError' is not convertible to 'NSError'; did you mean to use 'as!' to force downcast?
  • Checking if the SwiftError is NSError gives a warning that it always succeeds but fails runtime.
  • Casting SwiftError as an NSError gives a compiler error.

Can someone help explain to me why this happens and how could I know if a class implementing the Error protocol actually is an NSError or not ?

Thanks!

3

There are 3 best solutions below

3
matt On

There is something odd about the nature of the bridging between NSError and Error. They are bridged for communication purposes — that is, they can travel back and forth between Swift and Cocoa; and an error that comes from Cocoa is an NSError (and NSError adopts the Error protocol in Swift, to allow for this); but your class that you declare as conforming to Error is itself not an NSError.

because it doesn't respond to userInfo

If you need your Swift Error type to carry userInfo information for Cocoa's benefit, then you were looking for the CustomNSError protocol.

https://developer.apple.com/documentation/foundation/customnserror

0
Martin R On

This looks like a bug to me, I have filed it as SR-14322.

According to SE-0112 Improved NSError Bridging,

Every type that conforms to the Error protocol is implicitly bridged to NSError.

This works with struct and enum types, but apparently not with class types.

You can replace your class SwiftError: Error by a struct SwiftError: Error to solve the problem. If that is not possible for some reason then the following “trick” worked in my test:

let nsError = sError as Error as NSError

That compiles and gives the expected results:

class SwiftError: Error {}

extension SwiftError: CustomNSError {
    public static var errorDomain: String { "MyDomain" }
    public var errorCode: Int { 13 }
    public var errorUserInfo: [String : Any] { ["Foo" : "Bar" ] }
}

let sError = SwiftError()

let nsError = sError as Error as NSError
print(nsError.userInfo)
// Output: ["Foo": "Bar"]
0
yankat On

It may be related to the fact that Error protocol conforms to Sendable protocol, while a class type (not struct) has some special requirements to conform to Sendable, such as it must be a final class, etc...