Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 685
JoriDor Avatar asked Sep 12 '25 03:09

JoriDor


1 Answers

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.

like image 91
Rob Napier Avatar answered Sep 13 '25 16:09

Rob Napier