LocalizedError, RecoverableError, CustomNSError
Swift 2 introduced error handling by way of the throws, do, try and catch keywords. It was designed to work hand-in-hand with Cocoa error handling conventions, such that any type conforming to the Error protocol (since renamed to Error) was implicitly bridged to NSError and Objective-C methods with an NSError** parameter, were imported by Swift as throwing methods.
- (NSURL *)replace Item At URL:(NSURL *)url options:(NSFile Version Replacing Options)options error:(NSError * _Nullable *)error; func replace Item(at url: URL, options: NSFile Version.Replacing Options = []) throws -> URL For the most part, these changes offered a dramatic improvement over the status quo (namely, no error handling conventions in Swift at all). However, there were still a few gaps to fill to make Swift errors fully interoperable with Objective-C types. as described by Swift Evolution proposal SE-0112: “Improved NSError Bridging”.
Not long after these refinements landed in Swift 3, the practice of declaring errors in enumerations had become idiomatic.
Yet for how familiar we’ve all become with Error (née Error), surprisingly few of us are on a first-name basis with the other error protocols to come out of SE-0112. Like, when was the last time you came across Localized in the wild? How about Recoverable? Custom qu’est-ce que c’est?
At the risk of sounding cliché, you might say that these protocols are indeed pretty obscure, and there’s a good chance you haven’t heard of them:
LocalizedError -
A specialized error that provides localized messages describing the error and why it occurred.
RecoverableError -
A specialized error that may be recoverable by presenting several potential recovery options to the user.
CustomNSError -
A specialized error that provides a domain, error code, and user-info dictionary.
If you haven’t heard of any of these until now, you may be wondering when you’d ever use them. Well, as the adage goes, “There’s no time like the present”.
This week on NSHipster, we’ll take a quick look at each of these Swift Foundation error protocols and demonstrate how they can make your code — if not less error-prone — then more enjoyable in its folly.
Communicating Errors to the User
Too many cooks spoil the broth.
Consider the following Broth type with a nested Error enumeration and an initializer that takes a number of cooks and throws an error if that number is inadvisably large:
struct Broth { enum Error { case too Many Cooks(Int) } init(number Of Cooks: Int) throws { precondition(number Of Cooks > 0) guard number Of Cooks < redacted else { throw Error.too Many Cooks(number Of Cooks) } // ... proceed to make broth } } If an iOS app were to communicate an error resulting from broth spoiled by multitudinous cooks, it might do so with by presenting a UIAlert in a catch statement like this:
import UIKit class View Controller: UIView Controller { override func view Did Appear(_ animated: Bool) { super.view Did Appear(animated) do { self.broth = try Broth(number Of Cooks: 100) } catch let error as Broth.Error { let title: String let message: String switch error { case .too Many Cooks(let number Of Cooks): title = "Too Many Cooks (\(number Of Cooks))" message = """ It's difficult to reconcile many opinions. Reduce the number of decision makers. """ } let alert Controller = UIAlert Controller(title: title, message: message, preferred Style: .alert) alert Controller.add Action( UIAlert Action(title: "OK", style: .default) ) self.present(alert Controller, animated: true, completion: nil) } catch { // handle other errors... } } } Such an implementation, however, is at odds with well-understood boundaries between models and controllers. Not only does it create bloat in the controller, it also doesn’t scale to handling multiple errors or handling errors in multiple contexts.
To reconcile these anti-patterns, let’s turn to our first Swift Foundation error protocol.
Adopting the LocalizedError Protocol
The Localized protocol inherits the base Error protocol and adds four instance property requirements.
protocol Localized Error : Error { var error Description: String? { get } var failure Reason: String? { get } var recovery Suggestion: String? { get } var help Anchor: String? { get } } These properties map 1:1 with familiar NSError user keys.
| Requirement | User Info Key |
|---|---|
error | NSLocalized |
failure | NSLocalized |
recovery | NSLocalized |
help | NSHelp |
Let’s take another pass at our nested Broth.Error type and see how we might refactor error communication from the controller to instead be concerns of Localized conformance.
import Foundation extension Broth.Error: Localized Error { var error Description: String? { switch self { case .too Many Cooks(let number Of Cooks): return "Too Many Cooks (\(number Of Cooks))" } } var failure Reason: String? { switch self { case .too Many Cooks: return "It's difficult to reconcile many opinions." } } var recovery Suggestion: String? { switch self { case .too Many Cooks: return "Reduce the number of decision makers." } } } Using switch statements may be overkill for a single-case enumeration such as this, but it demonstrates a pattern that can be extended for more complex error types. Note also how pattern matching is used to bind the number constant to the associated value only when it’s necessary.
Now we can
import UIKit class View Controller: UIView Controller { override func view Did Appear(_ animated: Bool) { super.view Did Appear(animated) do { try make Broth(number Of Cooks: 100) } catch let error as Localized Error { let title = error.error Description let message = [ error.failure Reason, error.recovery Suggestion ].compact Map { $0 } .joined(separator: "\n\n") let alert Controller = UIAlert Controller(title: title, message: message, preferred Style: .alert) alert Controller.add Action( UIAlert Action(title: "OK", style: .default) ) self.present(alert Controller, animated: true, completion: nil) } catch { // handle other errors... } } } 
If that seems like a lot of work just to communicate an error to the user… you might be onto something.
Although UIKit borrowed many great conventions and idioms from AppKit, error handling wasn’t one of them. By taking a closer look at what was lost in translation, we’ll finally have the necessary context to understand the two remaining error protocols to be discussed.
Communicating Errors on macOS
If at first you don’t succeed, try, try again.
Communicating errors to users is significantly easier on macOS than on iOS. For example, you might construct and pass an NSError object to the present method, called on an NSWindow.
import App Kit @NSApplication Main class App Delegate: NSObject, NSApplication Delegate { @IBOutlet weak var window: NSWindow! func application Did Finish Launching(_ a Notification: Notification) { do { _ = try something() } catch { window.present Error(error) } } func something() throws -> Never { let user Info: [String: Any] = [ NSLocalized Description Key: NSLocalized String("The operation couldn’t be completed.", comment: "localized Error Description"), NSLocalized Recovery Suggestion Error Key: NSLocalized String("If at first you don't succeed...", comment: "localized Error Recover Suggestion") ] throw NSError(domain: "com.nshipster.error", code: 1, user Info: user Info) } } Doing so presents a modal alert dialog that fits right in with the rest of the system.

But macOS error handling isn’t merely a matter of convenient APIs; it also has built-in mechanisms for allowing users to select one of several options to attempt to resolve the reported issue.
Recovering from Errors
To turn a conventional NSError into one that supports recovery, you specify values for the user keys NSLocalized and NSRecovery. A great way to do that is to override the application(_:will delegate method and intercept and modify an error before it’s presented to the user.
extension App Delegate { func application(_ application: NSApplication, will Present Error error: Error) -> Error { var user Info: [String: Any] = (error as NSError).user Info user Info[NSLocalized Recovery Options Error Key] = [ NSLocalized String("Try, try again", comment: "try Again") NSLocalized String("Give up too easily", comment: "give Up") ] user Info[NSRecovery Attempter Error Key] = self return NSError(domain: (error as NSError).domain, code: (error as NSError).code, user Info: user Info) } } For NSLocalized, specify an array of one or more localized strings for each recovery option available the user.
For NSRecovery, set an object that implements the attempt method.
extension App Delegate { // MARK: NSError Recovery Attempting override func attempt Recovery(from Error error: Error, option Index recovery Option Index: Int) -> Bool { do { switch recovery Option Index { case 0: // Try, try again try something() case 1: fallthrough default: break } } catch { window.present Error(error) } return true } } With just a few lines of code, you’re able to facilitate a remarkably complex interaction, whereby a user is alerted to an error and prompted to resolve it according to a set of available options.

Cool as that is, it carries some pretty gross baggage. First, the attempt requirement is part of an informal protocol, which is effectively a handshake agreement that things will work as advertised. Second, the use of option indexes instead of actual objects makes for code that’s as fragile as it is cumbersome to write.
Fortunately, we can significantly improve on this by taking advantage of Swift’s superior type system and (at long last) the second subject of this article.
Modernizing Error Recovery with RecoverableError
The Recoverable protocol, like Localized is a refinement on the base Error protocol with the following requirements:
protocol Recoverable Error : Error { var recovery Options: [String] { get } func attempt Recovery(option Index recovery Option Index: Int, result Handler handler: @escaping (Bool) -> Void) func attempt Recovery(option Index recovery Option Index: Int) -> Bool } Also like Localized, these requirements map onto error user keys (albeit not as directly).
| Requirement | User Info Key |
|---|---|
recovery | NSLocalized |
attempt attempt | NSRecovery * |
The recovery property requirement is equivalent to the NSLocalized: an array of strings that describe the available options.
The attempt functions formalize the previously informal delegate protocol; func attempt is for “application” granularity, whereas attempt is for “document” granularity.
Supplementing RecoverableError with Additional Types
On its own, the Recoverable protocol improves only slightly on the traditional, NSError-based methodology by formalizing the requirements for recovery.
Rather than implementing conforming types individually, we can generalize the functionality with some clever use of generics.
First, define an Error protocol that re-casts the attempt methods from before to use an associated, Recovery type.
protocol Error Recovery Delegate: class { associatedtype Recovery Option: Custom String Convertible, Case Iterable func attempt Recovery(from error: Error, with option: Recovery Option) -> Bool } Requiring that Recovery conforms to Case, allows us to vend options directly to API consumers independently of their presentation to the user.
From here, we can define a generic Delegating type that wraps an Error type and associates it with the aforementioned Delegate, which is responsible for providing recovery options and attempting recovery with the one selected.
struct Delegating Recoverable Error<Delegate, Error>: Recoverable Error where Delegate: Error Recovery Delegate, Error: Swift.Error { let error: Error weak var delegate: Delegate? = nil init(recovering From error: Error, with delegate: Delegate?) { self.error = error self.delegate = delegate } var recovery Options: [String] { return Delegate.Recovery Option.all Cases.map { "\($0)" } } func attempt Recovery(option Index recovery Option Index: Int) -> Bool { let recovery Options = Delegate.Recovery Option.all Cases let index = recovery Options.index(recovery Options.start Index, offset By: recovery Option Index) let option = Delegate.Recovery Option.all Cases[index] return self.delegate?.attempt Recovery(from: self.error, with: option) ?? false } } Now we can refactor the previous example of our macOS app to have App conform to Error and define a nested Recovery enumeration with all of the options we wish to support.
extension App Delegate: Error Recovery Delegate { enum Recovery Option: String, Case Iterable, Custom String Convertible { case try Again case give Up var description: String { switch self { case .try Again: return NSLocalized String("Try, try again", comment: self.raw Value) case .give Up: return NSLocalized String("Give up too easily", comment: self.raw Value) } } } func attempt Recovery(from error: Error, with option: Recovery Option) -> Bool { do { if option == .try Again { try something() } } catch { window.present Error(error) } return true } func application(_ application: NSApplication, will Present Error error: Error) -> Error { return Delegating Recoverable Error(recovering From: error, with: self) } } The result?

…wait, that’s not right.
What’s missing? To find out, let’s look at our third and final protocol in our discussion.
Improving Interoperability with Cocoa Error Handling System
The Custom protocol is like an inverted NSError: it allows a type conforming to Error to act like it was instead an NSError subclass.
protocol Custom NSError: Error { static var error Domain: String { get } var error Code: Int { get } var error User Info: [String : Any] { get } } The protocol requirements correspond to the domain, code, and user properties of an NSError, respectively.
Now, back to our modal from before: normally, the title is taken from user via NSLocalized. Types conforming to Localized can provide this too through their equivalent error property. And while we could extend Delegating to adopt Localized, it’s actually much less work to add conformance for Custom:
extension Delegating Recoverable Error: Custom NSError { var error User Info: [String: Any] { return (self.error as NSError).user Info } } With this one additional step, we can now enjoy the fruits of our burden.

In programming, it’s often not what you know, but what you know about. Now that you’re aware of the existence of Localized, Recoverable, Custom, you’ll be sure to identify situations in which they might improve error handling in your app.
Useful AF, amiright? Then again, “Familiarity breeds contempt”; so often, what initially endears one to ourselves is what ultimately causes us to revile it.
Such is the error of our ways.