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.

As a refresher, the Objective-C style NSError class contains an error code and domain. In Swift, we don’t have this. Error is instead an (empty) protocol. The error “domain” is simply the 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 
}

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 equal to the error’s zero-based index in the enum, which seems to be swift’s default (internal) enum behavior. So the above enum actually represents is this:

enum HTTPError : Int, Error {
    case badRequest = 0
    case unauthorized = 1
    case forbidden = 2
    case notFound = 3 
    case internalServerError = 4
}

While this isn’t too hard to figure out, it’s not very explicit. If we have a large enum with a ton of cases it’s not efficient to waste developer time in order to trace the error back to the correct one. Additionally, if we add a new case in the middle of the list somewhere, all of the error codes following it will be altered. There is a solution to this problem: 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.

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.

Non-integer enums

As it turns out, the default swift 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

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)"
}