Swift Errors

Demystifying swift's error codes for custom classes.
, Updated: 5 min read

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

Subscribe to my Newsletter

Like this post? Subscribe to get notified for future posts like this.

Change Log

  • 1/25/2018 - Initial Revision
  • 3/26/2019 - updates to include better information about code / domain retrieval, clarified language, added tl;dr, and added correction about hashCode
  • 1/13/2022 - updates to reword no longer consistently accurate information about default enum tag values

Found a typo or technical problem? file an issue!