How many times have we looked at this code:
do {
try writeEverythingToDisk()
} catch let error {
// ???
}
or this one:
switch result {
case .failure(let error):
// ???
}
and asked themselves the question:
“How can I extract information from this error?”
The problem is that the error probably contains a lot of information that could help us. But obtaining this information is often not easy.
To understand the reason for this, let's look at the ways we have at our disposal to attach information to errors.
New: LocalizedError
In Swift, we pass errors according to the Error protocol. The LocalizedError protocol inherits it, extending it with some useful properties:
errorDescription
failureReason
recoverySuggestion
Compliance with the LocalizedError protocol instead of the Error protocol (and ensuring that these new properties are implemented) allows us to augment our error with a lot of useful information that can be passed at runtime (the NSHipster weblog covers this in more detail):
enum MyError: LocalizedError {
case badReference
var errorDescription: String? {
switch self {
case .badReference:
return "The reference was bad."
}
}
var failureReason: String? {
switch self {
case .badReference:
return "Bad Reference"
}
}
var recoverySuggestion: String? {
switch self {
case .badReference:
return "Try using a good one."
}
}
}
Old: userInfo
The well-known NSError class contains a property - a userInfo dictionary, which we can fill with whatever we want. But also, this dictionary contains several predefined keys:
NSLocalizedDescriptionKey
NSLocalizedFailureReasonErrorKey
NSLocalizedRecoverySuggestionErrorKey
You may notice that their names are very similar to the LocalizedError properties. And, in fact, they play a similar role:
let info = [
NSLocalizedDescriptionKey:
"The reference was bad.",
NSLocalizedFailureReasonErrorKey:
"Bad Reference",
NSLocalizedRecoverySuggestionErrorKey:
"Try using a good one."
]
let badReferenceNSError = NSError(
domain: "ReferenceDomain",
code: 42,
userInfo: info
)
It looks like LocalizedError and NSError should be basically the same, right? Well, therein lies the main problem.
Old meets new
The point is that the NSError class conforms to the Error protocol, but not to the LocalizedError protocol. In other words:
badReferenceNSError is NSError //> true
badReferenceNSError is Error //> true
badReferenceNSError is LocalizedError //> false
This means that if we try to extract information from any arbitrary error in the usual way, it will only work properly for Error and LocalizedError, but for NSError, only the value of the localizedDescription property will be reflected:
// The obvious way that doesn’t work:
func log(error: Error) {
print(error.localizedDescription)
if let localized = error as? LocalizedError {
print(localized.failureReason)
print(localized.recoverySuggestion)
}
}
log(error: MyError.badReference)
//> The reference was bad.
//> Bad Reference
//> Try using a good one.
log(error: badReferenceNSError)
//> The reference was bad.
This is rather annoying, because our NSError class object is known to contain information about the cause of the failure and a suggestion for correcting the error, registered in its userInfo dictionary. And this, for some reason, is not shown through the LocalizedError match.
New becomes old
At this point, we can get frustrated by mentally imagining a lot of switch statements trying to sort by type and by the presence of various properties in the userInfo dictionary. But don't be afraid! There is an easy solution. It's just not very obvious.
Note that the NSError class defines convenience methods for retrieving the localized description, failure reason, and recovery suggestion in the userInfo property:
badReferenceNSError.localizedDescription
//> "The reference was bad."
badReferenceNSError.localizedFailureReason
//> "Bad Reference"
badReferenceNSError.localizedRecoverySuggestion
//> "Try using a good one."
They are great for handling NSError, but they don't help us extract those values from LocalizedError...or is it?
It turns out that the Swift Error language protocol is connected by the compiler to the NSError class. This means we can turn an Error into an NSError with a simple cast:
let bridgedError: NSError
bridgedError = MyError.badReference as NSError
But what's even more impressive is that when we cast a LocalizedError this way, the bridge works properly and wires up localizedDescription, localizedFailureReason, and localizedRecoverySuggestion, pointing to the appropriate values!
So if we want a consistent interface to extract localized information from Error, LocalizedError, and NSError, we just need to cast everything to NSError without hesitation:
func log(error: Error) {
let bridge = error as NSError
print(bridge.localizedDescription)
print(bridge.localizedFailureReason)
print(bridge.localizedRecoverySuggestion)
}
log(error: MyError.badReference)
//> The reference was bad.
//> Bad Reference
//> Try using a good one.
log(error: badReferenceNSError)
//> The reference was bad.
//> Bad Reference
//> Try using a good one.
✌️
Top comments (0)