DEV Community

Cover image for Localized Error Meanings in Swift Practice
Vadim Atamanenko
Vadim Atamanenko

Posted on

Localized Error Meanings in Swift Practice

How many times have we looked at this code:

do {
  try writeEverythingToDisk()
} catch let error {
  // ???
}
Enter fullscreen mode Exit fullscreen mode

or this one:

switch result {
case .failure(let error):
  // ???
}
Enter fullscreen mode Exit fullscreen mode

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."
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

✌️

Top comments (0)