Search code examples
iosswiftprotocols

Swift: Checking conformance of Foundation (NS)Errors to a custom protocol


Use case:

To be able to log errors, we conform them to a certain protocol. This seems to work for custom errors, however, checking or casting Foundation errors (such as URLError) to this protocol, seems to be failing.

I have no clue what's different here.

In this example: We have a viewmodel that does an operation that can lead to an error. We want to log this error with our logger.

Example:


// Protocol that makes an error loggable

protocol LoggableError: Error {
    var message: String { get }
}

// Our custom error:

enum CustomError: Error {
    case someError
}

extension CustomError: LoggableError {
    var message: String {
        "Some error occurred"
    }
}

// URL error conforming to our LoggableError:

extension URLError: LoggableError {
    var message: String {
        "Some network error occurred"
    }
}

// The logger

protocol LoggerProtocol {
    func handle(error: some Error)
}

class Logger: LoggerProtocol {
    func handle(error: some Error) {
        guard let error = error as? LoggableError else { fatalError("Cast failed") }
        print(error.message)
    }
}

// The view model

class ViewModel {
    private let logger: LoggerProtocol

    init(logger: LoggerProtocol) {
        self.logger = logger
    }

    func doSomething() {
        do {
            try doSomethingDangerous()
        } catch {
            logger.handle(error: error)
        }
    }

    private func doSomethingDangerous() throws {
        throw CustomError.someError // this works
        // throw URLError(.notConnectedToInternet) // this triggers the fatalError
    }
}

// Trigger

let viewModel = ViewModel(logger: Logger())
viewModel.doSomething()

Can anyone tell me what I'm doing wrong or why this isn't working? 7 years of Swift development, I still don't seem to get my head around protocols :D


Solution

  • First, I consider this a bug in Foundation, and recommend opening a Feedback about it. But what you're encountering is due to the tricky ways that NSError is dealt with on Darwin (which is why you don't see this problem on Linux).

    Many Foundation errors are really just wrappers around NSError. Even though they have their own Swift type, they do not survive round-tripping through an existential:

    print(type(of: CustomError.someError))              // CustomError
    print(type(of: CustomError.someError as any Error)) // CustomError
    
    print(type(of: URLError(.notConnectedToInternet)))              // URLError
    print(type(of: URLError(.notConnectedToInternet) as any Error)) // **NSError**
    

    (Note that this is not related to enums vs structs. If you change CustomError to a struct, it still works fine. The problem is the NSError that you don't see.)

    Similar problems happen when coming through some. The ObjC and Swift types wind up not matching. There is magic bridging of Foundation errors to make them more "Swifty." But it's a pretty leaky abstraction, and this is a bug.

    If possible you should conform NSError to LoggableError. That will fix this issue.