ObjC interop - be careful when assigning NSUnderlyingError from Swift

Using the NSUnderlyingError pattern caused an unexpected crash for me.

In Swift I had defined an Error like this:

@objc
public class WrappingError: NSObject, CustomNSError {

    public let someDetails: String
    public let someMoreDetails: UInt32

    @objc
    public static let kSomeDetailsKey = "kSomeDetailsKey"

    @objc
    public static let kSomeMoreDetailsKey = "kSomeMoreDetailsKey"

    public let underlyingError: Error

    init(someDetails: String, someMoreDetails: UInt32, underlyingError: Error) {
        self.someDetails = someDetails
        self.someMoreDetails = someMoreDetails
        self.underlyingError = underlyingError
    }

    public var errorUserInfo: [String: Any] {
        return [
            type(of: self).someDetailsKey: someDetails,
            type(of: self).someMoreDetailsKey: someMoreDetails,
            NSUnderlyingErrorKey: underlyingError
        ]
    }
}

The crash occurred in objc in some code like this:

NSError *error;
[SomeSwiftClass methodThatThrowsAWrappingError:&error]
if ([error.domain isEqualTo:@"MyModule.WrappingError"]) {
    NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];

    // !!! "unknown selector" crash on next line, because `underlyingError`
    // is a Swift Error value, not an NSError
    if ([underlyingError.domain isEqualTo:@"FooError"]) {
        [self handleFooError];
    }
}

Am I correct in concluding that the conversion from a Swift Error to an NSError occurs as part of the throwing process, and thus if I want to do something like the above, the right thing would be to make a change to WrappingError like:

    public var errorUserInfo: [String: Any] {
        return [
            type(of: self).someDetailsKey: someDetails,
            type(of: self).someMoreDetailsKey: someMoreDetails,
            NSUnderlyingErrorKey: (underlyingError as NSError) // <- explicit cast
        ]
    }

This is a bug. What version of Swift are you using?

Swift 4.1

I can put together a demo app for you.

I believe this is already fixed in Swift 4.2. The same runtime behavior should be exercised by this test case:

import Foundation

enum Foo: Error { case x, y, z }

let x: Any = Foo.x

print((x as AnyObject) as! NSError)

IIRC this crashed in Swift 4.1, but in Swift 4.2, it correctly prints Error Domain=foo.Foo Code=0 "(null)" like an NSError would.

1 Like

You're right, I've verified that this works with Swift 4.2. Thanks!

No problem, thank you for confirming!