Localized Error Meanings in Swift Practice
Tags: swift, tutorial, practice, mobile
Published: 2022-12-17 05:14:25

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](https://nshipster.com/swift-foundation-error-protocols/)):

```
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.
```
✌️