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 debugging.
TL;DR: Add this extension to the Error protocol
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. As it turns out, the error code is [used to be] equal to the error’s zero-based index in the enum, which seems to be swift’s default (internal) enum behavior.
Edit: this may have changed in newer swift versions. For clarity, it’s best practice to tag the enum with some value in order to make the error meaningful, like mentioned below
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.
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 ##)
.
// (Swift 4) Extension to Error that allows for better debugging info than localizedDescription.
// https://www.richinfante.com/2018/01/25/demystifying-swift-errors
extension Error {
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 returns 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, what If we make other classes conform to Error?
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.
We could also make a type such as String conform to the Error protocol, and then throw it. It’s probably not a good idea for production use, but it’s an interesting use of the try/catch mechanism. If we do this, the error behavior is similar to a custom class.
extension String : Error {}
func playMovie() throws -> Never {
throw "Han shot first"
}
do {
try playMovie()
} catch let error as String {
print(error)
// Prints "Han shot first"
print((error as Error).debugDescription)
// Prints "String.Han shot first (code 1)"
}