diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5885ef5 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,23 @@ +name: CI + +on: + pull_request: + +jobs: + test-linux: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Run tests + run: swift test + + test-macos: + runs-on: macos-15 + + steps: + - uses: actions/checkout@v4 + + - name: Run tests + run: swift test diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b4ad1b --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# ErrorKit + +**ErrorKit** makes error handling in Swift more intuitive. It reduces boilerplate code while providing clearer insights. Helpful for users, fun for developers! + +*TODO: Add a list of advantages of using ErrorKit over Swift’s native error handling types.* + +--- + +## Why We Introduced the `Throwable` Protocol to Replace `Error` + +### The Confusing `Error` API + +Swift's `Error` protocol is simple – too simple. It has no requirements, but it offers one computed property, `localizedDescription`, which is often used to log errors or display messages to users. + +Consider the following example where we provide a `localizedDescription` for an enum: + +```swift +enum NetworkError: Error, CaseIterable { + case noConnectionToServer + case parsingFailed + + var localizedDescription: String { + switch self { + case .noConnectionToServer: "No connection to the server." + case .parsingFailed: "Data parsing failed." + } + } +} +``` + +You might expect this to work seamlessly, but it doesn’t. If we randomly throw an error and print its `localizedDescription`, like in the following SwiftUI view: + +```swift +struct ContentView: View { + var body: some View { + Button("Throw Random NetworkError") { + do { + throw NetworkError.allCases.randomElement()! + } catch { + print("Caught error with message: \(error.localizedDescription)") + } + } + } +} +``` + +The console output will surprise you: 😱 + +```bash +Caught error with message: The operation couldn’t be completed. (ErrorKitDemo.NetworkError error 0.) +``` + +There’s no information about the specific error case. Not even the enum case name appears, let alone the custom message! Why? Because Swift’s `Error` protocol is bridged to `NSError`, which uses `domain`, `code`, and `userInfo` instead. + +### The "Correct" Way: `LocalizedError` + +The correct approach is to conform to `LocalizedError`, which defines the following optional properties: + +- `errorDescription: String?` +- `failureReason: String?` +- `recoverySuggestion: String?` +- `helpAnchor: String?` + +However, since all of these properties are optional, you won’t get any compiler errors if you forget to implement them. Worse, only `errorDescription` affects `localizedDescription`. Fields like `failureReason` and `recoverySuggestion` are ignored, while `helpAnchor` is rarely used today. + +This makes `LocalizedError` both confusing and error-prone. + +### The Solution: `Throwable` + +To address these issues, **ErrorKit** introduces the `Throwable` protocol: + +```swift +public protocol Throwable: LocalizedError { + var localizedDescription: String { get } +} +``` + +This protocol is simple and clear. It’s named `Throwable` to align with Swift’s `throw` keyword and follows Swift’s convention of using the `able` suffix (like `Codable` and `Identifiable`). Most importantly, it requires the `localizedDescription` property, ensuring your errors behave exactly as expected. + +Here’s how you use it: + +```swift +enum NetworkError: Throwable { + case noConnectionToServer + case parsingFailed + + var localizedDescription: String { + switch self { + case .noConnectionToServer: "Unable to connect to the server." + case .parsingFailed: "Data parsing failed." + } + } +} +``` + +When you print `error.localizedDescription`, you'll get exactly the message you expect! 🥳 + +### Even Shorter Error Definitions + +Not all apps are localized, and developers may not have time to provide localized descriptions immediately. To make error handling even simpler, `Throwable` allows you to define your error messages using raw values: + +```swift +enum NetworkError: String, Throwable { + case noConnectionToServer = "Unable to connect to the server." + case parsingFailed = "Data parsing failed." +} +``` + +This approach eliminates boilerplate code while keeping the error definitions concise and descriptive. + +### Summary + +> Conform your custom error types to `Throwable` instead of `Error` or `LocalizedError`. The `Throwable` protocol requires only `localizedDescription: String`, ensuring your error messages are exactly what you expect – no surprises. diff --git a/Sources/ErrorKit/Throwable.swift b/Sources/ErrorKit/Throwable.swift new file mode 100644 index 0000000..841fdc0 --- /dev/null +++ b/Sources/ErrorKit/Throwable.swift @@ -0,0 +1,83 @@ +import Foundation + +/// A protocol that makes error handling in Swift more intuitive by requiring a user-friendly `localizedDescription` property. +/// +/// `Throwable` extends `LocalizedError` and simplifies the process of defining error messages, +/// ensuring that developers can provide meaningful feedback for errors without the confusion associated with Swift's native `Error` and `LocalizedError` types. +/// +/// ### Key Features: +/// - Requires a `localizedDescription`, making it easier to provide custom error messages. +/// - Offers a default implementation for `errorDescription`, ensuring smooth integration with `LocalizedError`. +/// - Supports `RawRepresentable` enums with `String` as `RawValue` to minimize boilerplate. +/// +/// ### Why Use `Throwable`? +/// - **Simplified API**: Unlike `LocalizedError`, `Throwable` focuses on a single requirement: `localizedDescription`. +/// - **Intuitive Naming**: The name aligns with Swift's `throw` keyword and other common `-able` protocols like `Codable`. +/// - **Readable Error Handling**: Provides concise, human-readable error descriptions. +/// +/// ### Usage Example: +/// +/// #### 1. Custom Error with Manual `localizedDescription`: +/// ```swift +/// enum NetworkError: Throwable { +/// case noConnectionToServer +/// case parsingFailed +/// +/// var localizedDescription: String { +/// switch self { +/// case .noConnectionToServer: "Unable to connect to the server." +/// case .parsingFailed: "Data parsing failed." +/// } +/// } +/// } +/// ``` +/// +/// #### 2. Custom Error Using `RawRepresentable` for Minimal Boilerplate: +/// ```swift +/// enum NetworkError: String, Throwable { +/// case noConnectionToServer = "Unable to connect to the server." +/// case parsingFailed = "Data parsing failed." +/// } +/// ``` +/// +/// #### 3. Throwing and Catching Errors: +/// ```swift +/// struct ContentView: View { +/// var body: some View { +/// Button("Throw Random NetworkError") { +/// do { +/// throw NetworkError.allCases.randomElement()! +/// } catch { +/// print("Caught error with message: \(error.localizedDescription)") +/// } +/// } +/// } +/// } +/// ``` +/// Output: +/// ``` +/// Caught error with message: Unable to connect to the server. +/// ``` +/// +public protocol Throwable: LocalizedError { + /// A human-readable error message describing the error. + var localizedDescription: String { get } +} + +// MARK: - Default Implementations + +/// Provides a default implementation for `Throwable` when the conforming type is a `RawRepresentable` with a `String` raw value. +/// +/// This allows enums with `String` raw values to automatically use the raw value as the error's `localizedDescription`. +extension Throwable where Self: RawRepresentable, RawValue == String { + public var localizedDescription: String { + self.rawValue + } +} + +/// Provides a default implementation for `errorDescription` required by `LocalizedError`, ensuring it returns the value of `localizedDescription`. +extension Throwable { + public var errorDescription: String? { + self.localizedDescription + } +}