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
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.