iosswiftcocoanserror

How to correctly bridge Swift Errors with arguments to NSError


I've read the Swift Evolution post on the improved bridging and some other sources online, but still something is missing.

Given this custom Error enum:

    public enum MyNetworkError: Error {
        case networkOffline
        case httpError(status: Int)
        case unknown
        case systemError(errno: Int)
    }

An Objective-C app should be able to read the error object and somehow extract the error name (networkOffline, httpError, unknown, systemError) and the case arguments (httpError.status and systemError.errno).

The result of converting the above to NSError is surprising, and I'm trying to understand how to improve things:

    let nse_A = MyNetworkError.networkOffline as NSError
    let nse_B = MyNetworkError.httpError(status: 503) as NSError
    let nse_C = MyNetworkError.unknown as NSError
    let nse_D = MyNetworkError.systemError(42) as NSError

First, the generated error codes. It appears that the cases that have arguments, regardless of their order, get code starting from zero:

    print(nse_A.code)  // 2 (expected: 0)
    print(nse_B.code)  // 0 (expected: 1)
    print(nse_C.code)  // 3 (expected: 2)
    print(nse_D.code)  // 1 (expected: 3)

Given an error code reported by the app, now it's harder to tell the actual error case.

Second, I expected such a smart mechanism (especially since it's compiler-generated) to also copy the case arguments to the userInfo dictionary - but it doesn't.

Am I doing it wrong, or do I have to fully implement the CustomNSError protocol to get meaningful and consistent NSError objects? That's an option, of course, but I expected it to be done automatically (kind of like how Codable does its magic).

In addition, can the app get the error case name as a String?

For reference, the above snippets were executed in a Xcode 10.3 Playground.


Solution

  • Short answer: For integer-based enum error types the error values are mapped to NSError codes as expected. For all other error types (like enums with associated values) you have to implement the CustomNSError protocol in order to control the NSError code and user info.

    Some details: Only for integer-based error types the code is bridged from Swift to NSError, for example:

    public enum IntNetworkError: Int, Error {
        case networkOffline = 13
        case httpError
        case unknown
        case systemError
    }
    
    let err = IntNetworkError.httpError as NSError
    print(err.code) // 14
    

    That is a special case implemented in ErrorType.swift. For all other error types, the default implementation is in ErrorDefaultImpls.cpp, and that returns the “tag” for enum types, and 1 for all other types. Example:

    struct StringError: Error {}
    
    let serr = StringError() as  NSError
    print(serr.code) // 1
    

    The enum “tag” is described in the Type Layout document. For enums with associated values, this does not necessarily follow the order in which the cases are declared. That is why you observed “unexpected” NSError codes.

    Therefore the correct approach is to implement the CustomNSError protocol, the compiler does not synthesize this for you.

    extension MyNetworkError: CustomNSError {
    
        public static var errorDomain: String {
            return "MyNetworkError"
        }
    
        public var errorCode: Int {
            switch self {
            case .networkOffline: return 1
            case .httpError: return 2
            case .unknown: return 3
            case .systemError: return 4
            }
        }
    
        public var errorUserInfo: [String : Any] {
            switch self {
            case .httpError(let status):
                return [ "status": status]
            case .systemError(let errno):
                return [ "errno": errno]
            default:
                return [:]
            }
        }
    }
    
    let nse_B = MyNetworkError.httpError(status: 503) as NSError
    
    print(nse_B.code) // 2
    print(nse_B.userInfo) // ["status": 503]