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.