Practical Localized Error Values in Swift
How many times have we stared at code like:
do {
try writeEverythingToDisk()
} catch let error {
// ???
}
or even:
switch result {
case .failure(let error):
// ???
}
and asked ourselves “How am I going to communicate that error?”
The thing is, that error likely contains a lot of information that could help us out. But getting to it is not always straight forward.
To see why, let’s look at the techniques at our disposal for attaching information to errors.
The New: LocalizedError
In Swift we pass around errors that conform to the Error
protocol. The LocalizedError
protocol inherits from this and extends it with some useful properties:
errorDescription
failureReason
recoverySuggestion
By conforming to LocalizedError
instead of Error
(and providing an implementation for these properties), we can stuff our error with a bunch of useful information that can be communicated at runtime (NSHipster goes way more in-depth on this):
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."
}
}
}
The Old: userInfo
Good old NSError
provides a userInfo
dictionary that we can fill with anything we want. But it also provides some predefined keys:
NSLocalizedDescriptionKey
NSLocalizedFailureReasonErrorKey
NSLocalizedRecoverySuggestionErrorKey
We might note these are very similar in name to the properties LocalizedError
provides. And, in fact, they perform 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
)
So seems like LocalizedError
and NSError
ought to be mostly interchangeable then, right? Well, therein lies the rub.
The Old Meets the New
See, NSError
implements Error
, but not LocalizedError
. Which is to say:
badReferenceNSError is NSError //> true
badReferenceNSError is Error //> true
badReferenceNSError is LocalizedError //> false
This means if we try to get data out of some unknown error in the obvious way, it works as expected for Error
and LocalizedError
, but only reports a localizedDescription
for NSError
:
// 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.
That’s pretty annoying because we know our NSError
has a failure reason and recovery suggestion defined in its userInfo
. It’s just, for whatever reason, not exposed via LocalizedError
conformance.
The New Becomes the Old
At this point we may despair, picturing long switch
statements trying to sort types and test existence of various userInfo
properties. But never fear! There is an easy solution. It’s just non-obvious.
See, NSError
has convenience methods defined on it for extracting localized description, failure reason, and recovery suggestion from the userInfo
:
badReferenceNSError.localizedDescription
//> "The reference was bad."
badReferenceNSError.localizedFailureReason
//> "Bad Reference"
badReferenceNSError.localizedRecoverySuggestion
//> "Try using a good one."
Which is great for handling an NSError
, but doesn’t help us get these values out of a Swift LocalizedError
… or does it?
It turns out Swift’s Error
is bridged by the compiler to NSError
. Which means we can treat an Error
as an NSError
with a simple cast:
let bridgedError: NSError
bridgedError = MyError.badReference as NSError
More impressively, though, when we cast a LocalizedError
in this way, the bridge does the right thing and wires up localizedDescription
, localizedFailureReason
, and localizedRecoverySuggestion
to point to the appropriate values!
So if we want a consistent interface to pull localized information out of Error
, LocalizedError
, and NSError
, we just need to blindly cast everything to an NSError
first:
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.
Once we have all this delicious context about our errors, what should we do with it? That’s the topic of next week’s post. See you then!