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)