ErrorKit: Fixing Swift's Error Handling Limitations and Unlocking New Possibilities

Hello Swift community!

I'm excited to introduce ErrorKit, a library I've been developing over the past 8 months that addresses several longstanding challenges with error handling in Swift.

The Problem

Many Swift developers have experienced the frustration of writing custom error messages only to see them ignored when caught:

enum NetworkError: Error {
   case noConnectionToServer
   
   var localizedDescription: String {
      "No connection to the server."
   }
}

// Later, when caught and printed:
// "The operation couldn't be completed. (YourPackage.NetworkError error 0.)"

This issue stems from Swift's Error protocol's bridging to NSError, which doesn't respect your custom properties as you might expect.

The Solution: Throwable Protocol

ErrorKit introduces the Throwable protocol that integrates correctly with Swift's error system:

enum NetworkError: Throwable {
   case noConnectionToServer
   
   var userFriendlyMessage: String {
      String(localized: "Unable to connect to the server.")
   }
}

// Now when caught and printed:
// "Unable to connect to the server."

For rapid development, you can use string raw values:

enum NetworkError: String, Throwable {
   case noConnectionToServer = "Unable to connect to the server."
   case parsingFailed = "Data parsing failed."
}

But That's Just The Beginning

ErrorKit offers a complete suite of error handling improvements:

:magnifying_glass_tilted_left: Enhanced Error Descriptions

Get improved, user-friendly messages for ANY error, including mapping cryptic system errors to more understandable messages for users:

do {
    let _ = try Data(contentsOf: url)
} catch {
    // Maps NSURLErrorDomain errors to clearer messages
    print(ErrorKit.userFriendlyMessage(for: error))
    // "You are not connected to the Internet. Please check your connection."
    // Instead of "The operation couldn't be completed. (NSURLErrorDomain error -1009.)"
}

:chains: Error Chain Debugging

Trace how errors propagate through your application:

Logger().error("\(ErrorKit.errorChainDescription(for: error))")
// ProfileError
// └─ DatabaseError
//    └─ FileError.notFound(path: "/Users/data.db")
//       └─ userFriendlyMessage: "Could not find database file."

:package: Built-in Error Types

Use standardized error types for common scenarios:

func fetchData() throws(NetworkError) -> Data {
    guard isNetworkReachable() else {
        throw .noInternet
    }

    // ...
    
    guard response.statusCode == 200 else {
        throw .serverError(
            code: response.statusCode,
            message: response.errorMessage
        )
    }
    
    // ...
}

Includes ready-to-use types like NetworkError, DatabaseError, FileError, ValidationError, PermissionError, and more.

:shield: Swift 6 Typed Throws Support

The Catching protocol solves nested errors with typed throws:

enum ProfileError: Throwable, Catching {
    case validationFailed(field: String)
    case caught(Error)  // Single case handles all nested errors
    
    var userFriendlyMessage: String { /* ... */ }
}

func loadProfile(id: String) throws(ProfileError) -> UserProfile {
    // First perform validation and throw specific error
    guard id.isValidFormat else {
        throw ProfileError.validationFailed(field: "id")
    }
    
    // Automatically wrap any database or file errors
    let userData = try ProfileError.catch {
        let user = try database.loadUser(id)
        let settings = try fileSystem.readUserSettings(user.settingsPath)
        return UserProfile(user: user, settings: settings)
    }
    
    // use the returned data here
}

:mobile_phone: User Feedback with Error Logs

Simplify gathering diagnostic information:

Button("Report a Problem") {
    showMailComposer = true
}
.mailComposer(
    isPresented: $showMailComposer,
    recipient: "support@yourapp.com",
    subject: "Bug Report",
    messageBody: "Please describe what happened:",
    attachments: [
        try? ErrorKit.logAttachment(ofLast: .minutes(30))
    ]
)

Gradual Adoption

Each feature can be adopted independently. Start with the Throwable protocol, then gradually incorporate other features as needed.

Documentation

The examples above are just the tip of the iceberg! The library is extensively documented with detailed guides, API references, and practical examples for each feature.

Full documentation is available on Swift Package Index.

I also started a series of articles with more details on the problems ErrorKit solves:

I welcome your feedback and contributions to make error handling in Swift more intuitive and powerful for everyone!

Find the GitHub repo here:

11 Likes

This looks awesome! Creating something like this has been at the back of my mind for quite a while, and it looks like you’ve addressed many of the issues that have long annoyed me. I look forward to trying this out in a few of my projects!

2 Likes

I used CutomNSError for that., e.g.:

enum NetworkError: Error, CustomNSError {
    case noConnectionToServer
   
    var localizedDescription: String {
       "No connection to the server."
    }
    var errorUserInfo: [String : Any] {
        [NSLocalizedDescriptionKey : localizedDescription]
    }
}

FWIF it's also useful in situations when you want to customise error code (e.g. where error type is a struct type or an enum with cases with associated payload).