Swift Errors

An improved way of getting debug strings for Swift Error instances, and demystifying error codes for custom error enums
, 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 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.

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
  • 10/27/2024 - rework contents for clarity, and not to bury the lede

Found a typo or technical problem? file an issue!