Here, I explore Swift’s error handling system and how it relates to NSError, and propose a simple extension to the Error protocol that allows for improved slightly clearer debugging, especially if dealing with enums without associated raw values.
TL;DR: Adding this extension to the Error protocol, which adds a .debugDescription
property that includes the error type, enum case, and the error enum value, instead of just the type name/code.
// Extension to Error that allows for better debugging info than localizedDescription.
// https://www.richinfante.com/2018/01/25/demystifying-swift-errors
extension Error {
/// Return a debug description, including the error type name, enum key (if a Swift error), and its error code.
/// Output format: ErrorType.errorCase (code ##)
var debugDescription: String {
return "\(String(describing: type(of: self))).\(String(describing: self)) (code \((self as NSError).code))"
}
}
Practical Example
Let’s dive into a quick example on how this improves the developer debugging experience.
Consider this error enum without associated values, direct from the Apple Swift Docs. We have one of these errors, printing it out as a string for logging purposes isn’t very clear.
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
Built-In .localizedDescription
Result
The default localizedDescription value isn’t super helpful. You’ll get a result that looks something like this, which may not be stable if the enum’s list of cases changes across app versions:
VendingMachineError.invalidSelection.localizedDescription
// Returns:
// "The operation couldn’t be completed. (VendingMachineError error 1.)"
New .debugDescription
Result
Using my above protocol extension’s debugDescription property gives you the enum key, so you aren’t left counting which entry in the enum it is. On first glance, this is much more clear:
VendingMachineError.invalidSelection.debugDescription
// Returns:
// "VendingMachineError.invalidSelection (code 1)"
If you have a situation where you have a compiled app that’s outputting an error string, with this extension you don’t need to reason about which version of the source code it was built from. This could be solved in some cases (see further below) by adding associated values to the enum, but that doesn’t work if the enum has arguments like the above example.
Now, we’ll dive into some background and how/why this works.
How, Why, and Background
As a refresher, the Objective-C style NSError
class contains an error code
and domain
. In Swift, we don’t have this. Error
is a protocol, with a single member named localizedDescription
, which is equivalent to NSError’s localizedDescription
property. The error “domain” is simply the name of whatever type conforms to the error protocol. But what about the error codes? In some situations, it may be necessary to have a generic error handler and report meaningful debug information to the developer. While error codes still exist, but are a little more difficult to analyze at first glance.
Let’s say you’re following Apple’s documentation and you build a custom error enum that looks something like this:
enum HTTPError : Error {
case badRequest
case unauthorized
case forbidden
case notFound
case internalServerError
}
It’s not terrible, but if we want to get a human-readable description of the error when it’s thrown, you’d get something like this:
The key HTTPError.notFound.localizedDescription
returns the string:
“The operation couldn’t be completed. (HTTPError error 3.)”
That’s not too helpful. “error 3” doesn’t obviously relate to the original error enum at first glance. In a lot of cases, the error code is simply the index within the enum. However, this might not always be the case, as some readers have written me and explained this may not be true in some scenarios.
If we make our enum a RawRepresentable Int
enum, we can then customize our error codes to our liking. This makes it much more explicit and easier for debugging, and ensures they are preserved in between versions. Note - this is not possible if you have an enum with arguments:
enum HTTPError : Int, Error {
case badRequest = 400
case unauthorized = 401
case forbidden = 403
case notFound = 404
case internalServerError = 500
}
HTTPError.notFound.localizedDescription
now returns the following value:
“The operation couldn’t be completed. (HTTPError error 404.)”
This is a bit better, now we can easily trace an error back to the enum that it’s declared in from the string description. If we decide to add a new response code, it won’t affect the error codes for the rest of them.
Improved Error Description
Can we improve this more? As it turns out, we can fetch the error code for any Error instance by casting to NSError. Additionally, if we use Swift’s String(describing:)
initializer, we can get a string representing the enum key that represents the error. We can then create an extension that produces error codes in the format ErrorType.errorCase (code ##)
.
// Extension to Error that allows for better debugging info than localizedDescription.
// https://www.richinfante.com/2018/01/25/demystifying-swift-errors
extension Error {
/// Return a debug description, including the error type name, enum key (if a Swift error), and its error code.
/// Output format: ErrorType.errorCase (code ##)
var debugDescription: String {
return "\(String(describing: type(of: self))).\(String(describing: self)) (code \((self as NSError).code))"
}
}
Using our custom extension, we can now get more information about the error. The key HTTPError.notFound.debugDescription
returns the string:
“HTTPError.notFound (code 404)“
This is much clearer for debugging and more explicit about what error was thrown. This helps a lot if we’re sending error information to a crash reporter and we can add extra information about what happened.
How It Works:
First, casting to NSError allows us to extract some information about the error code. The domain
is less useful (but could be used).
let domain = (HTTPError.badRequest as NSError).domain
// prints "test.HTTPError" (depends on compiler settings / code environment)
// Domain for most projects appears to be formatted like: `{ProductName}.{TypeName}`.
// For playgrounds, this is similar to `__lldb_expr_5.HTTPError`:
print(domain)
let code = (HTTPError.badRequest as NSError).code
print (code) // prints "400" / the raw value.
Next, we describe the type of the error, as well as our instance to get names to display:
let enumName = String(describing: type(of: HTTPError.badRequest))
print(enumName) // Prints "HTTPError"
let typeName = String(describing: HTTPError.badRequest)
print(typeName) // Prints "badRequest
Non-integer enums
The default Swift indexing behavior for error codes seems to return if you use a non-integer enum.
enum AnotherError : String, Error {
case bad = "Something Bad Happened"
case terrible = "Something Terrible Happened"
case critical = "Error level is critical"
}
Running AnotherError.critical.localizedDescription
returns:
“The operation couldn’t be completed. (AnotherError error 2.)”
Side note which may be useful: Although we shouldn’t rely on hash values being the same between different executions of the same program, it appears that a Swift enum’s hashValue is set to the index of the item in the enum, regardless of its actual Raw type. So in our AnotherError
example above, the AnotherError.critical.hashValue
is equal to: (AnotherError.critical as NSError).code
2019-03-26
- NOTE: Further investigation reveals this is not always (or is no longer) be the case. It used to be the observed behavior, but appears to no longer work.
Other Types
In Swift 4, Error is defined as an empty protocol. The only requirement for try/catch ing a value is that the thrown value conforms to the Error protocol. So, we can make our own error classes. How do these work with the above protocol extension?
class CustomError : Error {}
CustomError().localizedDescription // = "The operation couldn’t be completed. (CustomError error 1.)"
It appears that the error code is always “1” for custom class implementations.