├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .spi.yml ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ ├── ErrorKit-Package.xcscheme │ ├── ErrorKit.xcscheme │ └── ErrorKitClient.xcscheme ├── LICENSE ├── Logo.png ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── ErrorKit │ ├── BuiltInErrors │ ├── DatabaseError.swift │ ├── FileError.swift │ ├── GenericError.swift │ ├── NetworkError.swift │ ├── OperationError.swift │ ├── ParsingError.swift │ ├── PermissionError.swift │ ├── StateError.swift │ └── ValidationError.swift │ ├── Catching.swift │ ├── ErrorKit.docc │ ├── ErrorKit.md │ ├── Guides │ │ ├── Built-in-Error-Types.md │ │ ├── Enhanced-Error-Descriptions.md │ │ ├── Error-Chain-Debugging.md │ │ ├── Throwable-Protocol.md │ │ ├── Typed-Throws-and-Error-Nesting.md │ │ └── User-Feedback-with-Logs.md │ ├── Resources │ │ ├── BuiltInErrorTypes.jpg │ │ ├── EnhancedDescriptions.jpg │ │ ├── ErrorChainDebugging.jpg │ │ ├── ErrorKit.png │ │ ├── Logo.png │ │ ├── ThrowableProtocol.jpg │ │ ├── TypedThrowsAndNesting.jpg │ │ └── UserFeedbackWithLogs.jpg │ └── theme-settings.json │ ├── ErrorKit.swift │ ├── ErrorMapper.swift │ ├── ErrorMappers │ ├── CoreDataErrorMapper.swift │ ├── FoundationErrorMapper.swift │ └── MapKitErrorMapper.swift │ ├── Helpers │ ├── Logger+ErrorKit.swift │ └── String+ErrorKit.swift │ ├── Logging │ ├── ErrorKit+OSLog.swift │ ├── MailAttachment.swift │ ├── MailComposerModifier.swift │ └── MailComposerView.swift │ ├── Resources │ ├── Localizable.xcstrings │ └── PrivacyInfo.xcprivacy │ ├── Throwable.swift │ └── TypedOverloads │ ├── FileManager+ErrorKit.swift │ └── URLSession+ErrorKit.swift └── Tests └── ErrorKitTests ├── ErrorKitTests.swift └── ThrowableTests.swift /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | 6 | indent_style = space 7 | tab_width = 6 8 | indent_size = 3 9 | 10 | end_of_line = lf 11 | insert_final_newline = true 12 | 13 | max_line_length = 160 14 | trim_trailing_whitespace = true 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | test-macos: 8 | runs-on: macos-15 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Run tests 14 | run: swift test 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [ErrorKit] 5 | swift_version: 6.0 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/ErrorKit-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 62 | 68 | 69 | 70 | 71 | 72 | 82 | 83 | 89 | 90 | 91 | 92 | 98 | 99 | 105 | 106 | 107 | 108 | 110 | 111 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/ErrorKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/ErrorKitClient.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 70 | 76 | 77 | 78 | 79 | 85 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2024-2025 FlineDev (alias Cihat Gündüz) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/ErrorKit/94d08765d65a7fc7b64730b0a9920ce4246d9fa0/Logo.png -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "a19e9fac572b01a6c4a13f3c05b6840358467e265b1a3c3b33a3612c5fbdf41d", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-asn1", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-asn1.git", 8 | "state" : { 9 | "revision" : "a54383ada6cecde007d374f58f864e29370ba5c3", 10 | "version" : "1.3.2" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-crypto", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/apple/swift-crypto.git", 17 | "state" : { 18 | "revision" : "e8d6eba1fef23ae5b359c46b03f7d94be2f41fed", 19 | "version" : "3.12.3" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ErrorKit", 6 | defaultLocalization: "en", 7 | platforms: [.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v16)], 8 | products: [.library(name: "ErrorKit", targets: ["ErrorKit"])], 9 | dependencies: [ 10 | // CryptoKit is not available on Linux, so we need Swift Crypto 11 | .package(url: "https://github.com/apple/swift-crypto.git", from: "3.11.0"), 12 | ], 13 | targets: [ 14 | .target( 15 | name: "ErrorKit", 16 | dependencies: [ 17 | .product( 18 | name: "Crypto", 19 | package: "swift-crypto", 20 | condition: .when(platforms: [.android, .linux, .openbsd, .wasi, .windows]) 21 | ), 22 | ], 23 | resources: [ 24 | .process("Resources/Localizable.xcstrings"), 25 | .process("Resources/PrivacyInfo.xcprivacy"), 26 | ] 27 | ), 28 | .testTarget(name: "ErrorKitTests", dependencies: ["ErrorKit"]), 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ErrorKit Logo](https://github.com/FlineDev/ErrorKit/blob/main/Logo.png?raw=true) 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FFlineDev%2FErrorKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/FlineDev/ErrorKit) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FFlineDev%2FErrorKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/FlineDev/ErrorKit) 5 | 6 | # ErrorKit 7 | 8 | Making error handling in Swift more intuitive and powerful with clearer messages, type safety, and user-friendly diagnostics. 9 | 10 | ## Overview 11 | 12 | Swift's error handling has several limitations that make it challenging to create robust, user-friendly applications: 13 | - The `Error` protocol's confusing behavior with `localizedDescription` 14 | - Hard-to-understand system error messages 15 | - Limited type safety in error propagation 16 | - Difficulties with error chain debugging (relevant for typed throws!) 17 | - Challenges in collecting meaningful feedback from users 18 | 19 | ErrorKit addresses these challenges with a suite of lightweight, interconnected features you can adopt progressively. 20 | 21 | ## Core Features 22 | 23 | ### The Throwable Protocol 24 | 25 | `Throwable` fixes the confusion of Swift's `Error` protocol by providing a clear, Swift-native approach to error handling: 26 | 27 | ```swift 28 | enum NetworkError: Throwable { 29 | case noConnectionToServer 30 | case parsingFailed 31 | 32 | var userFriendlyMessage: String { 33 | switch self { 34 | case .noConnectionToServer: 35 | String(localized: "Unable to connect to the server.") 36 | case .parsingFailed: 37 | String(localized: "Data parsing failed.") 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | Now when catching this error, you'll see exactly what you expect: 44 | ``` 45 | "Unable to connect to the server." 46 | ``` 47 | 48 | For rapid development, you can use string raw values: 49 | 50 | ```swift 51 | enum NetworkError: String, Throwable { 52 | case noConnectionToServer = "Unable to connect to the server." 53 | case parsingFailed = "Data parsing failed." 54 | } 55 | ``` 56 | 57 | [Read more about Throwable →](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/throwable-protocol) 58 | 59 | ### Enhanced Error Descriptions 60 | 61 | Get improved, user-friendly messages for ANY error, including system errors: 62 | 63 | ```swift 64 | do { 65 | let _ = try Data(contentsOf: url) 66 | } catch { 67 | // Better than localizedDescription, works with any error type 68 | print(ErrorKit.userFriendlyMessage(for: error)) 69 | // "You are not connected to the Internet. Please check your connection." 70 | 71 | // String interpolation automatically uses userFriendlyMessage(for:) 72 | print("Request failed: \(error)") 73 | } 74 | ``` 75 | 76 | These enhanced descriptions are community-provided and fully localized mappings of common system errors to clearer, more actionable messages. ErrorKit comes with built-in mappers for Foundation, CoreData, MapKit, and more. You can also create custom mappers for third-party libraries or your own error types. 77 | 78 | [Read more about Enhanced Error Descriptions →](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/enhanced-error-descriptions) 79 | 80 | ## Swift 6 Typed Throws Support 81 | 82 | Swift 6 introduces typed throws (`throws(ErrorType)`), bringing compile-time type checking to error handling. ErrorKit makes this powerful feature practical with solutions for its biggest challenges: 83 | 84 | ### Error Nesting with Catching 85 | 86 | The `Catching` protocol solves the biggest problem with error handling: nested errors. 87 | 88 | ```swift 89 | enum ProfileError: Throwable, Catching { 90 | case validationFailed(field: String) 91 | case caught(Error) // Single case handles all nested errors! 92 | 93 | var userFriendlyMessage: String { /* ... */ } 94 | } 95 | 96 | struct ProfileRepository { 97 | func loadProfile(id: String) throws(ProfileError) -> UserProfile { 98 | // Regular error throwing for validation 99 | guard id.isValidFormat else { 100 | throw ProfileError.validationFailed(field: "id") 101 | } 102 | 103 | // Automatically wrap any database or file errors 104 | let userData = try ProfileError.catch { 105 | let user = try database.loadUser(id) 106 | let settings = try fileSystem.readUserSettings(user.settingsPath) 107 | return UserProfile(user: user, settings: settings) 108 | } 109 | 110 | return userData 111 | } 112 | } 113 | ``` 114 | 115 | [Read more about Typed Throws and Error Nesting →](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/typed-throws-and-error-nesting) 116 | 117 | ### Error Chain Debugging 118 | 119 | When using `Throwable` with the `Catching` protocol, you get powerful error chain debugging: 120 | 121 | ```swift 122 | do { 123 | try await updateUserProfile() 124 | } catch { 125 | print(ErrorKit.errorChainDescription(for: error)) 126 | 127 | // Output shows the complete error path: 128 | // ProfileError 129 | // └─ DatabaseError 130 | // └─ FileError.notFound(path: "/Users/data.db") 131 | // └─ userFriendlyMessage: "Could not find database file." 132 | } 133 | ``` 134 | 135 | [Read more about Error Chain Debugging →](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/error-chain-debugging) 136 | 137 | ## Ready-to-Use Tools 138 | 139 | ### Built-in Error Types 140 | 141 | Stop reinventing common error types in every project. ErrorKit provides standardized error types for common scenarios: 142 | 143 | ```swift 144 | func fetchUserData() throws(DatabaseError) { 145 | guard isConnected else { 146 | throw .connectionFailed 147 | } 148 | // Fetching logic 149 | } 150 | ``` 151 | 152 | Includes ready-to-use types like `DatabaseError`, `NetworkError`, `FileError`, `ValidationError`, `PermissionError`, and more – all conforming to both `Throwable` and `Catching` with localized messages. 153 | 154 | For quick one-off errors, use `GenericError`: 155 | 156 | ```swift 157 | func quickOperation() throws { 158 | guard condition else { 159 | throw GenericError(userFriendlyMessage: "The operation couldn't be completed due to invalid state.") 160 | } 161 | // Operation logic 162 | } 163 | ``` 164 | 165 | [Read more about Built-in Error Types →](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/built-in-error-types) 166 | 167 | ### User Feedback with Error Logs 168 | 169 | Gathering diagnostic information from users has never been simpler: 170 | 171 | ```swift 172 | Button("Report a Problem") { 173 | showMailComposer = true 174 | } 175 | .mailComposer( 176 | isPresented: $showMailComposer, 177 | recipient: "support@yourapp.com", 178 | subject: "Bug Report", 179 | messageBody: "Please describe what happened:", 180 | attachments: [ 181 | try? ErrorKit.logAttachment(ofLast: .minutes(30)) 182 | ] 183 | ) 184 | ``` 185 | 186 | With just a simple built-in SwiftUI modifier and the `logAttachment` helper function, you can easily include all log messages from Apple's unified logging system and let your users send them to you via email. Other integrations are also supported. 187 | 188 | [Read more about User Feedback and Logging →](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/user-feedback-with-logs) 189 | 190 | ## How These Features Work Together 191 | 192 | ErrorKit's features are designed to complement each other while remaining independently useful: 193 | 194 | 1. **Start with improved error definitions** using `Throwable` for custom errors and `userFriendlyMessage(for:)` for system errors. 195 | 196 | 2. **Add type safety with Swift 6 typed throws**, using the `Catching` protocol to solve nested error challenges. This pairs with error chain debugging to understand error flows through your app. 197 | 198 | 3. **Save time with ready-made tools**: built-in error types for common scenarios and simple log collection for user feedback. 199 | 200 | 4. **Extend with custom mappers**: Create error mappers for any library to improve error messages across your entire application. 201 | 202 | ## Adoption Path 203 | 204 | Here's a practical adoption strategy: 205 | 206 | 1. Replace `Error` with `Throwable` in your custom error types 207 | 2. Use `ErrorKit.userFriendlyMessage(for:)` when showing system errors 208 | 3. Adopt built-in error types where they fit your needs 209 | 4. Implement typed throws with `Catching` for more robust error flows 210 | 5. Add error chain debugging to improve error visibility 211 | 6. Integrate log collection with your feedback system 212 | 213 | ## Documentation 214 | 215 | For complete documentation visit: 216 | [ErrorKit Documentation](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit) 217 | 218 | ## Showcase 219 | 220 | I created this library for my own Indie apps (download & rate them to show your appreciation): 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 234 | 241 | 242 | 243 | 244 | 249 | 256 | 257 | 258 | 259 | 264 | 271 | 272 | 273 | 274 | 279 | 286 | 287 | 288 | 289 | 294 | 301 | 302 | 303 | 304 | 309 | 316 | 317 | 318 | 319 | 324 | 331 | 332 | 333 |
App IconApp Name & DescriptionSupported Platforms
230 | 231 | 232 | 233 | 235 | 236 | TranslateKit: App Localization 237 | 238 |
239 | AI-powered app localization with unmatched accuracy. Fast & easy: AI & proofreading, 125+ languages, market insights. Budget-friendly, free to try. 240 |
Mac
245 | 246 | 247 | 248 | 250 | 251 | FreemiumKit: In-App Purchases for Indies 252 | 253 |
254 | Simple In-App Purchases and Subscriptions: Automation, Paywalls, A/B Testing, Live Notifications, PPP, and more. 255 |
iPhone, iPad, Mac, Vision
260 | 261 | 262 | 263 | 265 | 266 | Pleydia Organizer: Movie & Series Renamer 267 | 268 |
269 | Simple, fast, and smart media management for your Movie, TV Show and Anime collection. 270 |
Mac
275 | 276 | 277 | 278 | 280 | 281 | FreelanceKit: Project Time Tracking 282 | 283 |
284 | Simple & affordable time tracking with a native experience for all devices. iCloud sync & CSV export included. 285 |
iPhone, iPad, Mac, Vision
290 | 291 | 292 | 293 | 295 | 296 | CrossCraft: Custom Crosswords 297 | 298 |
299 | Create themed & personalized crosswords. Solve them yourself or share them to challenge others. 300 |
iPhone, iPad, Mac, Vision
305 | 306 | 307 | 308 | 310 | 311 | FocusBeats: Pomodoro + Music 312 | 313 |
314 | Deep Focus with proven Pomodoro method & select Apple Music playlists & themes. Automatically pauses music during breaks. 315 |
iPhone, iPad, Mac, Vision
320 | 321 | 322 | 323 | 325 | 326 | Posters: Discover Movies at Home 327 | 328 |
329 | Auto-updating & interactive posters for your home with trailers, showtimes, and links to streaming services. 330 |
Vision
334 | -------------------------------------------------------------------------------- /Sources/ErrorKit/BuiltInErrors/DatabaseError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents errors that occur during database operations. 4 | /// 5 | /// # Examples of Use 6 | /// 7 | /// ## Handling Database Connections 8 | /// ```swift 9 | /// struct DatabaseConnection { 10 | /// func connect() throws(DatabaseError) { 11 | /// guard let socket = openNetworkSocket() else { 12 | /// throw .connectionFailed 13 | /// } 14 | /// // Successful connection logic 15 | /// } 16 | /// } 17 | /// ``` 18 | /// 19 | /// ## Managing Record Operations 20 | /// ```swift 21 | /// struct UserRepository { 22 | /// func findUser(byId id: String) throws(DatabaseError) -> User { 23 | /// guard let user = database.findUser(id: id) else { 24 | /// throw .recordNotFound(entity: "User", identifier: id) 25 | /// } 26 | /// return user 27 | /// } 28 | /// 29 | /// func updateUser(_ user: User) throws(DatabaseError) { 30 | /// guard hasValidPermissions(for: user) else { 31 | /// throw .operationFailed(context: "Updating user profile") 32 | /// } 33 | /// // Update logic 34 | /// } 35 | /// } 36 | /// ``` 37 | public enum DatabaseError: Throwable, Catching { 38 | /// The database connection failed. 39 | /// 40 | /// # Example 41 | /// ```swift 42 | /// struct AuthenticationService { 43 | /// func authenticate() throws(DatabaseError) { 44 | /// guard let connection = attemptDatabaseConnection() else { 45 | /// throw .connectionFailed 46 | /// } 47 | /// // Proceed with authentication 48 | /// } 49 | /// } 50 | /// ``` 51 | case connectionFailed 52 | 53 | /// The database query failed to execute. 54 | /// 55 | /// # Example 56 | /// ```swift 57 | /// struct AnalyticsRepository { 58 | /// func generateReport(for period: DateInterval) throws(DatabaseError) -> Report { 59 | /// guard period.duration <= maximumReportPeriod else { 60 | /// throw .operationFailed(context: "Generating analytics report") 61 | /// } 62 | /// // Report generation logic 63 | /// } 64 | /// } 65 | /// ``` 66 | /// - Parameters: 67 | /// - context: A description of the operation or entity being queried. 68 | case operationFailed(context: String) 69 | 70 | /// A requested record was not found in the database. 71 | /// 72 | /// # Example 73 | /// ```swift 74 | /// struct ProductInventory { 75 | /// func fetchProduct(sku: String) throws(DatabaseError) -> Product { 76 | /// guard let product = database.findProduct(bySKU: sku) else { 77 | /// throw .recordNotFound(entity: "Product", identifier: sku) 78 | /// } 79 | /// return product 80 | /// } 81 | /// } 82 | /// ``` 83 | /// - Parameters: 84 | /// - entity: The name of the entity or record type. 85 | /// - identifier: A unique identifier for the missing entity. 86 | case recordNotFound(entity: String, identifier: String?) 87 | 88 | /// Generic error message if the existing cases don't provide the required details. 89 | /// 90 | /// # Example 91 | /// ```swift 92 | /// struct DataMigrationService { 93 | /// func migrate() throws(DatabaseError) { 94 | /// guard canPerformMigration() else { 95 | /// throw .generic(userFriendlyMessage: "Migration cannot be performed") 96 | /// } 97 | /// // Migration logic 98 | /// } 99 | /// } 100 | /// ``` 101 | case generic(userFriendlyMessage: String) 102 | 103 | /// Represents a child error (either from your own error types or unknown system errors) that was wrapped into this error type. 104 | /// This case is used internally by the ``catch(_:)`` function to store any errors thrown by the wrapped code. 105 | /// 106 | /// # Example 107 | /// ```swift 108 | /// struct UserRepository { 109 | /// func fetchUserDetails(id: String) throws(DatabaseError) { 110 | /// // Check if user exists - simple case with explicit error 111 | /// guard let user = database.findUser(id: id) else { 112 | /// throw DatabaseError.recordNotFound(entity: "User", identifier: id) 113 | /// } 114 | /// 115 | /// // Any errors from parsing or file access are automatically wrapped 116 | /// let preferences = try DatabaseError.catch { 117 | /// let prefsData = try fileManager.contents(ofFile: user.preferencesPath) 118 | /// return try JSONDecoder().decode(UserPreferences.self, from: prefsData) 119 | /// } 120 | /// 121 | /// // Use the loaded preferences 122 | /// user.applyPreferences(preferences) 123 | /// } 124 | /// } 125 | /// ``` 126 | /// 127 | /// The `caught` case stores the original error while maintaining type safety through typed throws. 128 | /// Instead of manually catching and wrapping unknown errors, use the ``catch(_:)`` function 129 | /// which automatically wraps any thrown errors into this case. 130 | /// 131 | /// - Parameters: 132 | /// - error: The original error that was wrapped into this error type. 133 | case caught(Error) 134 | 135 | /// A user-friendly error message suitable for display to end users. 136 | public var userFriendlyMessage: String { 137 | switch self { 138 | case .connectionFailed: 139 | return String.localized( 140 | key: "BuiltInErrors.DatabaseError.connectionFailed", 141 | defaultValue: "Unable to establish a connection to the database. Check your network settings and try again." 142 | ) 143 | case .operationFailed(let context): 144 | return String.localized( 145 | key: "BuiltInErrors.DatabaseError.operationFailed", 146 | defaultValue: "The database operation for \(context) could not be completed. Please retry the action." 147 | ) 148 | case .recordNotFound(let entity, let identifier): 149 | if let identifier { 150 | return String.localized( 151 | key: "BuiltInErrors.DatabaseError.recordNotFoundWithID", 152 | defaultValue: "The \(entity) record with ID \(identifier) was not found in the database. Verify the details and try again." 153 | ) 154 | } else { 155 | return String.localized( 156 | key: "BuiltInErrors.DatabaseError.recordNotFound", 157 | defaultValue: "The \(entity) record was not found in the database. Verify the details and try again." 158 | ) 159 | } 160 | case .generic(let userFriendlyMessage): 161 | return userFriendlyMessage 162 | case .caught(let error): 163 | return ErrorKit.userFriendlyMessage(for: error) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Sources/ErrorKit/BuiltInErrors/FileError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents errors that occur during file operations. 4 | /// 5 | /// # Examples of Use 6 | /// 7 | /// ## Handling File Retrieval 8 | /// ```swift 9 | /// struct DocumentManager { 10 | /// func loadDocument(named name: String) throws(FileError) -> Document { 11 | /// guard let fileURL = findFile(named: name) else { 12 | /// throw .fileNotFound(fileName: name) 13 | /// } 14 | /// // Document loading logic 15 | /// } 16 | /// } 17 | /// ``` 18 | /// 19 | /// ## Managing File Operations 20 | /// ```swift 21 | /// struct FileProcessor { 22 | /// func processFile(at path: String) throws(FileError) { 23 | /// guard canWrite(to: path) else { 24 | /// throw .writeFailed(fileName: path) 25 | /// } 26 | /// // File processing logic 27 | /// } 28 | /// 29 | /// func readConfiguration() throws(FileError) -> Configuration { 30 | /// guard let data = attemptFileRead() else { 31 | /// throw .readFailed(fileName: "config.json") 32 | /// } 33 | /// // Configuration parsing logic 34 | /// } 35 | /// } 36 | /// ``` 37 | public enum FileError: Throwable, Catching { 38 | /// The file could not be found. 39 | /// 40 | /// # Example 41 | /// ```swift 42 | /// struct AssetManager { 43 | /// func loadImage(named name: String) throws(FileError) -> Image { 44 | /// guard let imagePath = searchForImage(name) else { 45 | /// throw .fileNotFound(fileName: name) 46 | /// } 47 | /// // Image loading logic 48 | /// } 49 | /// } 50 | /// ``` 51 | case fileNotFound(fileName: String) 52 | 53 | /// There was an issue reading the file. 54 | /// 55 | /// # Example 56 | /// ```swift 57 | /// struct LogReader { 58 | /// func readLatestLog() throws(FileError) -> String { 59 | /// guard let logContents = attemptFileRead() else { 60 | /// throw .readFailed(fileName: "application.log") 61 | /// } 62 | /// return logContents 63 | /// } 64 | /// } 65 | /// ``` 66 | case readFailed(fileName: String) 67 | 68 | /// There was an issue writing to the file. 69 | /// 70 | /// # Example 71 | /// ```swift 72 | /// struct DataBackup { 73 | /// func backup(data: Data) throws(FileError) { 74 | /// guard canWriteToBackupLocation() else { 75 | /// throw .writeFailed(fileName: "backup.dat") 76 | /// } 77 | /// // Backup writing logic 78 | /// } 79 | /// } 80 | /// ``` 81 | case writeFailed(fileName: String) 82 | 83 | /// Generic error message if the existing cases don't provide the required details. 84 | /// 85 | /// # Example 86 | /// ```swift 87 | /// struct FileIntegrityChecker { 88 | /// func validateFile() throws(FileError) { 89 | /// guard passes(integrityCheck) else { 90 | /// throw .generic(userFriendlyMessage: "File integrity compromised") 91 | /// } 92 | /// // Validation logic 93 | /// } 94 | /// } 95 | /// ``` 96 | case generic(userFriendlyMessage: String) 97 | 98 | /// An error that occurred during a file operation, wrapped into this error type using the ``catch(_:)`` function. 99 | /// This could include system-level file errors, encoding/decoding errors, or any other errors encountered during file operations. 100 | /// 101 | /// # Example 102 | /// ```swift 103 | /// struct DocumentStorage { 104 | /// func saveDocument(_ document: Document) throws(FileError) { 105 | /// // Regular error for missing file 106 | /// guard fileExists(document.path) else { 107 | /// throw FileError.fileNotFound(fileName: document.name) 108 | /// } 109 | /// 110 | /// // Automatically wrap encoding and file system errors 111 | /// try FileError.catch { 112 | /// let data = try JSONEncoder().encode(document) 113 | /// try data.write(to: document.url, options: .atomic) 114 | /// } 115 | /// } 116 | /// } 117 | /// ``` 118 | /// 119 | /// The `caught` case stores the original error while maintaining type safety through typed throws. 120 | /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function 121 | /// which automatically wraps any thrown errors into this case. 122 | /// 123 | /// - Parameters: 124 | /// - error: The original error that occurred during the file operation. 125 | case caught(Error) 126 | 127 | /// A user-friendly error message suitable for display to end users. 128 | public var userFriendlyMessage: String { 129 | switch self { 130 | case .fileNotFound(let fileName): 131 | return String.localized( 132 | key: "BuiltInErrors.FileError.fileNotFound", 133 | defaultValue: "The file \(fileName) could not be located. Please verify the file path and try again." 134 | ) 135 | case .readFailed(let fileName): 136 | return String.localized( 137 | key: "BuiltInErrors.FileError.readError", 138 | defaultValue: "An error occurred while attempting to read the file \(fileName). Please check file permissions and try again." 139 | ) 140 | case .writeFailed(let fileName): 141 | return String.localized( 142 | key: "BuiltInErrors.FileError.writeError", 143 | defaultValue: "Unable to write to the file \(fileName). Ensure you have the necessary permissions and try again." 144 | ) 145 | case .generic(let userFriendlyMessage): 146 | return userFriendlyMessage 147 | case .caught(let error): 148 | return ErrorKit.userFriendlyMessage(for: error) 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/ErrorKit/BuiltInErrors/GenericError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a generic error with a custom user-friendly message. 4 | /// Use this when the built-in error types don't match your specific error case 5 | /// or when you need a simple way to throw custom error messages. 6 | /// 7 | /// # Examples of Use 8 | /// 9 | /// ## Custom Business Logic Validation 10 | /// ```swift 11 | /// struct BusinessRuleValidator { 12 | /// func validateComplexRule(data: BusinessData) throws(GenericError) { 13 | /// guard meetsCustomCriteria(data) else { 14 | /// throw GenericError( 15 | /// userFriendlyMessage: String(localized: "The business data doesn't meet required criteria") 16 | /// ) 17 | /// } 18 | /// 19 | /// // Automatically wrap any other errors if needed 20 | /// try GenericError.catch { 21 | /// try validateAdditionalRules(data) 22 | /// } 23 | /// } 24 | /// } 25 | /// ``` 26 | /// 27 | /// ## Application-Specific Errors 28 | /// ```swift 29 | /// struct CustomProcessor { 30 | /// func processSpecialCase() throws(GenericError) { 31 | /// guard canHandleSpecialCase() else { 32 | /// throw GenericError( 33 | /// userFriendlyMessage: String(localized: "Unable to process this special case") 34 | /// ) 35 | /// } 36 | /// // Special case handling 37 | /// } 38 | /// } 39 | /// ``` 40 | public struct GenericError: Throwable, Catching { 41 | /// A user-friendly message describing the error. 42 | public let userFriendlyMessage: String 43 | 44 | /// Creates a new generic error with a custom user-friendly message. 45 | /// 46 | /// # Example 47 | /// ```swift 48 | /// struct CustomValidator { 49 | /// func validateSpecialRequirement() throws(GenericError) { 50 | /// guard meetsRequirement() else { 51 | /// throw GenericError( 52 | /// userFriendlyMessage: String(localized: "The requirement was not met. Please try again.") 53 | /// ) 54 | /// } 55 | /// // Validation logic 56 | /// } 57 | /// } 58 | /// ``` 59 | /// - Parameter userFriendlyMessage: A clear, actionable message that will be shown to the user. 60 | public init(userFriendlyMessage: String) { 61 | self.userFriendlyMessage = userFriendlyMessage 62 | } 63 | 64 | /// Creates a new generic error that wraps another error. 65 | /// Used internally by the ``catch(_:)`` function to automatically wrap any thrown errors. 66 | /// 67 | /// # Example 68 | /// ```swift 69 | /// struct FileProcessor { 70 | /// func processUserData() throws(GenericError) { 71 | /// // Explicit throwing for validation 72 | /// guard isValidPath(userDataPath) else { 73 | /// throw GenericError(userFriendlyMessage: "Invalid file path selected") 74 | /// } 75 | /// 76 | /// // Automatically wrap any file system or JSON errors 77 | /// let userData = try GenericError.catch { 78 | /// let data = try Data(contentsOf: userDataPath) 79 | /// return try JSONDecoder().decode(UserData.self, from: data) 80 | /// } 81 | /// } 82 | /// } 83 | /// ``` 84 | /// 85 | /// - Parameter error: The error to be wrapped. 86 | public static func caught(_ error: Error) -> Self { 87 | GenericError(userFriendlyMessage: ErrorKit.userFriendlyMessage(for: error)) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/ErrorKit/BuiltInErrors/NetworkError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents errors that can occur during network operations. 4 | /// 5 | /// # Examples of Use 6 | /// 7 | /// ## Handling Network Connectivity 8 | /// ```swift 9 | /// struct NetworkService { 10 | /// func fetchData() throws(NetworkError) -> Data { 11 | /// guard isNetworkReachable() else { 12 | /// throw .noInternet 13 | /// } 14 | /// // Network request logic 15 | /// } 16 | /// } 17 | /// ``` 18 | /// 19 | /// ## Managing API Requests 20 | /// ```swift 21 | /// struct APIClient { 22 | /// func makeRequest(to endpoint: URL) throws(NetworkError) -> T { 23 | /// guard let response = performRequest(endpoint) else { 24 | /// throw .timeout 25 | /// } 26 | /// 27 | /// guard response.statusCode == 200 else { 28 | /// throw .badRequest( 29 | /// code: response.statusCode, 30 | /// message: response.errorMessage 31 | /// ) 32 | /// } 33 | /// 34 | /// guard let decodedData = try? JSONDecoder().decode(T.self, from: response.data) else { 35 | /// throw .decodingFailure 36 | /// } 37 | /// 38 | /// return decodedData 39 | /// } 40 | /// } 41 | /// ``` 42 | public enum NetworkError: Throwable, Catching { 43 | /// No internet connection is available. 44 | /// 45 | /// # Example 46 | /// ```swift 47 | /// struct OfflineContentManager { 48 | /// func syncContent() throws(NetworkError) { 49 | /// guard isNetworkAvailable() else { 50 | /// throw .noInternet 51 | /// } 52 | /// // Synchronization logic 53 | /// } 54 | /// } 55 | /// ``` 56 | /// - Note: This error may occur if the device is in airplane mode or lacks network connectivity. 57 | case noInternet 58 | 59 | /// The request timed out before completion. 60 | /// 61 | /// # Example 62 | /// ```swift 63 | /// struct ImageDownloader { 64 | /// func download(from url: URL) throws(NetworkError) -> Image { 65 | /// guard let image = performDownloadWithTimeout() else { 66 | /// throw .timeout 67 | /// } 68 | /// return image 69 | /// } 70 | /// } 71 | /// ``` 72 | case timeout 73 | 74 | /// The server responded with a bad request error. 75 | /// 76 | /// # Example 77 | /// ```swift 78 | /// struct UserProfileService { 79 | /// func updateProfile(_ profile: UserProfile) throws(NetworkError) { 80 | /// let response = sendUpdateRequest(profile) 81 | /// guard response.isSuccess else { 82 | /// throw .badRequest( 83 | /// code: response.statusCode, 84 | /// message: response.errorMessage 85 | /// ) 86 | /// } 87 | /// // Update success logic 88 | /// } 89 | /// } 90 | /// ``` 91 | /// - Parameters: 92 | /// - code: The exact HTTP status code returned by the server. 93 | /// - message: An error message provided by the server in the body. 94 | case badRequest(code: Int, message: String) 95 | 96 | /// The server responded with a general server-side error. 97 | /// 98 | /// # Example 99 | /// ```swift 100 | /// struct PaymentService { 101 | /// func processPayment(_ payment: Payment) throws(NetworkError) { 102 | /// let response = submitPayment(payment) 103 | /// guard response.isSuccessful else { 104 | /// throw .serverError( 105 | /// code: response.statusCode, 106 | /// message: response.errorMessage 107 | /// ) 108 | /// } 109 | /// // Payment processing logic 110 | /// } 111 | /// } 112 | /// ``` 113 | /// - Parameters: 114 | /// - code: The HTTP status code returned by the server. 115 | /// - message: An optional error message provided by the server. 116 | case serverError(code: Int, message: String?) 117 | 118 | /// The response could not be decoded or parsed. 119 | /// 120 | /// # Example 121 | /// ```swift 122 | /// struct DataTransformer { 123 | /// func parseResponse(_ data: Data) throws(NetworkError) -> T { 124 | /// guard let parsed = try? JSONDecoder().decode(T.self, from: data) else { 125 | /// throw .decodingFailure 126 | /// } 127 | /// return parsed 128 | /// } 129 | /// } 130 | /// ``` 131 | case decodingFailure 132 | 133 | /// Generic error message if the existing cases don't provide the required details. 134 | /// 135 | /// # Example 136 | /// ```swift 137 | /// struct UnexpectedErrorHandler { 138 | /// func handle(_ error: Error) throws(NetworkError) { 139 | /// guard !isHandledError(error) else { 140 | /// throw .generic(userFriendlyMessage: "An unexpected network error occurred") 141 | /// } 142 | /// // Error handling logic 143 | /// } 144 | /// } 145 | /// ``` 146 | case generic(userFriendlyMessage: String) 147 | 148 | /// An error that occurred during a network operation, wrapped into this error type using the ``catch(_:)`` function. 149 | /// This could include URLSession errors, SSL/TLS errors, or any other errors encountered during network communication. 150 | /// 151 | /// # Example 152 | /// ```swift 153 | /// struct APIClient { 154 | /// func fetchUserProfile(id: String) throws(NetworkError) { 155 | /// // Regular error for no connectivity 156 | /// guard isNetworkReachable else { 157 | /// throw NetworkError.noInternet 158 | /// } 159 | /// 160 | /// // Automatically wrap URLSession and decoding errors 161 | /// let profile = try NetworkError.catch { 162 | /// let (data, response) = try await URLSession.shared.data(from: userProfileURL) 163 | /// return try JSONDecoder().decode(UserProfile.self, from: data) 164 | /// } 165 | /// } 166 | /// } 167 | /// ``` 168 | /// 169 | /// The `caught` case stores the original error while maintaining type safety through typed throws. 170 | /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function 171 | /// which automatically wraps any thrown errors into this case. 172 | /// 173 | /// - Parameters: 174 | /// - error: The original error that occurred during the network operation. 175 | case caught(Error) 176 | 177 | /// A user-friendly error message suitable for display to end users. 178 | public var userFriendlyMessage: String { 179 | switch self { 180 | case .noInternet: 181 | return String.localized( 182 | key: "BuiltInErrors.NetworkError.noInternet", 183 | defaultValue: "Unable to connect to the internet. Please check your network settings and try again." 184 | ) 185 | case .timeout: 186 | return String.localized( 187 | key: "BuiltInErrors.NetworkError.timeout", 188 | defaultValue: "The network request took too long to complete. Please check your connection and try again." 189 | ) 190 | case .badRequest(let code, let message): 191 | return String.localized( 192 | key: "BuiltInErrors.NetworkError.badRequest", 193 | defaultValue: "There was an issue with the request (Code: \(code)). \(message). Please review and retry." 194 | ) 195 | case .serverError(let code, let message): 196 | let defaultMessage = String.localized( 197 | key: "BuiltInErrors.NetworkError.serverError", 198 | defaultValue: "The server encountered an error (Code: \(code)). " 199 | ) 200 | 201 | if let message = message { 202 | return defaultMessage + message 203 | } else { 204 | return defaultMessage + String.localized( 205 | key: "Common.Message.tryAgainLater", 206 | defaultValue: "Please try again later." 207 | ) 208 | } 209 | case .decodingFailure: 210 | return String.localized( 211 | key: "BuiltInErrors.NetworkError.decodingFailure", 212 | defaultValue: "Unable to process the server's response. Please try again or contact support if the issue persists." 213 | ) 214 | case .generic(let userFriendlyMessage): 215 | return userFriendlyMessage 216 | case .caught(let error): 217 | return ErrorKit.userFriendlyMessage(for: error) 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /Sources/ErrorKit/BuiltInErrors/OperationError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents errors related to failed or invalid operations. 4 | /// 5 | /// # Examples of Use 6 | /// 7 | /// ## Handling Operation Dependencies 8 | /// ```swift 9 | /// struct DataProcessingPipeline { 10 | /// func runComplexOperation() throws(OperationError) { 11 | /// guard checkDependencies() else { 12 | /// throw .dependencyFailed(dependency: "Cache Initialization") 13 | /// } 14 | /// // Operation processing logic 15 | /// } 16 | /// } 17 | /// ``` 18 | /// 19 | /// ## Managing Cancelable Operations 20 | /// ```swift 21 | /// struct BackgroundTask { 22 | /// func performLongRunningTask() throws(OperationError) { 23 | /// guard !isCancellationRequested() else { 24 | /// throw .canceled 25 | /// } 26 | /// // Long-running task logic 27 | /// } 28 | /// } 29 | /// ``` 30 | public enum OperationError: Throwable, Catching { 31 | /// The operation could not start due to a dependency failure. 32 | /// 33 | /// # Example 34 | /// ```swift 35 | /// struct DataSynchronizer { 36 | /// func synchronize() throws(OperationError) { 37 | /// guard isNetworkReady() else { 38 | /// throw .dependencyFailed(dependency: "Network Connection") 39 | /// } 40 | /// // Synchronization logic 41 | /// } 42 | /// } 43 | /// ``` 44 | /// - Parameter dependency: A description of the failed dependency. 45 | case dependencyFailed(dependency: String) 46 | 47 | /// The operation was canceled before completion. 48 | /// 49 | /// # Example 50 | /// ```swift 51 | /// struct ImageProcessor { 52 | /// func processImage() throws(OperationError) { 53 | /// guard !userRequestedCancel else { 54 | /// throw .canceled 55 | /// } 56 | /// // Image processing logic 57 | /// } 58 | /// } 59 | /// ``` 60 | case canceled 61 | 62 | /// Generic error message if the existing cases don't provide the required details. 63 | /// 64 | /// # Example 65 | /// ```swift 66 | /// struct GenericErrorManager { 67 | /// func handleSpecialCase() throws(OperationError) { 68 | /// guard !isHandledCase() else { 69 | /// throw .generic(userFriendlyMessage: "A unique operation error occurred") 70 | /// } 71 | /// // Special case handling 72 | /// } 73 | /// } 74 | /// ``` 75 | case generic(userFriendlyMessage: String) 76 | 77 | /// An error that occurred during an operation execution, wrapped into this error type using the ``catch(_:)`` function. 78 | /// This could include task cancellation errors, operation queue errors, or any other errors encountered during complex operations. 79 | /// 80 | /// # Example 81 | /// ```swift 82 | /// struct DataProcessor { 83 | /// func processLargeDataset(_ dataset: Dataset) throws(OperationError) { 84 | /// // Regular error for operation prerequisites 85 | /// guard meetsMemoryRequirements(dataset) else { 86 | /// throw OperationError.dependencyFailed(dependency: "Memory Requirements") 87 | /// } 88 | /// 89 | /// // Automatically wrap operation and processing errors 90 | /// let result = try OperationError.catch { 91 | /// let operation = try ProcessingOperation(dataset) 92 | /// try operation.validateInputs() 93 | /// return try operation.execute() 94 | /// } 95 | /// } 96 | /// } 97 | /// ``` 98 | /// 99 | /// The `caught` case stores the original error while maintaining type safety through typed throws. 100 | /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function 101 | /// which automatically wraps any thrown errors into this case. 102 | /// 103 | /// - Parameters: 104 | /// - error: The original error that occurred during the operation. 105 | case caught(Error) 106 | 107 | /// A user-friendly error message suitable for display to end users. 108 | public var userFriendlyMessage: String { 109 | switch self { 110 | case .dependencyFailed(let dependency): 111 | return String.localized( 112 | key: "BuiltInErrors.OperationError.dependencyFailed", 113 | defaultValue: "The operation could not be started because a required component failed to initialize: \(dependency). Please restart the application or contact support." 114 | ) 115 | case .canceled: 116 | return String.localized( 117 | key: "BuiltInErrors.OperationError.canceled", 118 | defaultValue: "The operation was canceled at your request. You can retry the action if needed." 119 | ) 120 | case .generic(let userFriendlyMessage): 121 | return userFriendlyMessage 122 | case .caught(let error): 123 | return ErrorKit.userFriendlyMessage(for: error) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/ErrorKit/BuiltInErrors/ParsingError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents errors that occur during parsing of input or data. 4 | /// 5 | /// # Examples of Use 6 | /// 7 | /// ## Handling Input Validation 8 | /// ```swift 9 | /// struct JSONParser { 10 | /// func parse(rawInput: String) throws(ParsingError) -> ParsedData { 11 | /// guard isValidJSONFormat(rawInput) else { 12 | /// throw .invalidInput(input: rawInput) 13 | /// } 14 | /// // Parsing logic 15 | /// } 16 | /// } 17 | /// ``` 18 | /// 19 | /// ## Managing Structured Data Parsing 20 | /// ```swift 21 | /// struct UserProfileParser { 22 | /// func parseProfile(data: [String: Any]) throws(ParsingError) -> UserProfile { 23 | /// guard let username = data["username"] else { 24 | /// throw .missingField(field: "username") 25 | /// } 26 | /// // Profile parsing logic 27 | /// } 28 | /// } 29 | /// ``` 30 | public enum ParsingError: Throwable, Catching { 31 | /// The input was invalid and could not be parsed. 32 | /// 33 | /// # Example 34 | /// ```swift 35 | /// struct ConfigurationParser { 36 | /// func parseConfig(input: String) throws(ParsingError) -> Configuration { 37 | /// guard isValidConfigurationFormat(input) else { 38 | /// throw .invalidInput(input: input) 39 | /// } 40 | /// // Configuration parsing logic 41 | /// } 42 | /// } 43 | /// ``` 44 | /// - Parameter input: The invalid input string or description. 45 | case invalidInput(input: String) 46 | 47 | /// A required field was missing in the input. 48 | /// 49 | /// # Example 50 | /// ```swift 51 | /// struct PaymentProcessor { 52 | /// func validatePaymentDetails(_ details: [String: String]) throws(ParsingError) { 53 | /// guard details["cardNumber"] != nil else { 54 | /// throw .missingField(field: "cardNumber") 55 | /// } 56 | /// // Payment processing logic 57 | /// } 58 | /// } 59 | /// ``` 60 | /// - Parameter field: The name of the missing field. 61 | case missingField(field: String) 62 | 63 | /// Generic error message if the existing cases don't provide the required details. 64 | /// 65 | /// # Example 66 | /// ```swift 67 | /// struct UnexpectedParsingHandler { 68 | /// func handleUnknownParsingIssue() throws(ParsingError) { 69 | /// guard !isHandledCase() else { 70 | /// throw .generic(userFriendlyMessage: "An unexpected parsing error occurred") 71 | /// } 72 | /// // Fallback error handling 73 | /// } 74 | /// } 75 | /// ``` 76 | case generic(userFriendlyMessage: String) 77 | 78 | /// An error that occurred during parsing or data transformation, wrapped into this error type using the ``catch(_:)`` function. 79 | /// This could include JSON decoding errors, format validation errors, or any other errors encountered during data parsing. 80 | /// 81 | /// # Example 82 | /// ```swift 83 | /// struct ProfileParser { 84 | /// func parseUserProfile(data: Data) throws(ParsingError) { 85 | /// // Regular error for missing data 86 | /// guard !data.isEmpty else { 87 | /// throw ParsingError.missingField(field: "profile_data") 88 | /// } 89 | /// 90 | /// // Automatically wrap JSON decoding and validation errors 91 | /// let profile = try ParsingError.catch { 92 | /// let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] 93 | /// return try UserProfile(validating: json) 94 | /// } 95 | /// } 96 | /// } 97 | /// ``` 98 | /// 99 | /// The `caught` case stores the original error while maintaining type safety through typed throws. 100 | /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function 101 | /// which automatically wraps any thrown errors into this case. 102 | /// 103 | /// - Parameters: 104 | /// - error: The original error that occurred during the parsing operation. 105 | case caught(Error) 106 | 107 | /// A user-friendly error message suitable for display to end users. 108 | public var userFriendlyMessage: String { 109 | switch self { 110 | case .invalidInput(let input): 111 | return String.localized( 112 | key: "BuiltInErrors.ParsingError.invalidInput", 113 | defaultValue: "The provided input could not be processed correctly: \(input). Please review the input and ensure it matches the expected format." 114 | ) 115 | case .missingField(let field): 116 | return String.localized( 117 | key: "BuiltInErrors.ParsingError.missingField", 118 | defaultValue: "The required information is incomplete. The \(field) field is missing and must be provided to continue." 119 | ) 120 | case .generic(let userFriendlyMessage): 121 | return userFriendlyMessage 122 | case .caught(let error): 123 | return ErrorKit.userFriendlyMessage(for: error) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/ErrorKit/BuiltInErrors/PermissionError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents errors related to missing or denied permissions. 4 | /// 5 | /// # Examples of Use 6 | /// 7 | /// ## Handling Permission Checks 8 | /// ```swift 9 | /// struct LocationService { 10 | /// func requestLocation() throws(PermissionError) { 11 | /// switch checkLocationPermission() { 12 | /// case .denied: 13 | /// throw .denied(permission: "Location") 14 | /// case .restricted: 15 | /// throw .restricted(permission: "Location") 16 | /// case .notDetermined: 17 | /// throw .notDetermined(permission: "Location") 18 | /// case .authorized: 19 | /// // Proceed with location request 20 | /// } 21 | /// } 22 | /// } 23 | /// ``` 24 | /// 25 | /// ## Managing Permission Workflows 26 | /// ```swift 27 | /// struct CameraAccessManager { 28 | /// func verifyAndRequestCameraAccess() throws(PermissionError) { 29 | /// guard canRequestCameraPermission() else { 30 | /// throw .restricted(permission: "Camera") 31 | /// } 32 | /// // Permission request logic 33 | /// } 34 | /// } 35 | /// ``` 36 | public enum PermissionError: Throwable, Catching { 37 | /// The user denied the required permission. 38 | /// 39 | /// # Example 40 | /// ```swift 41 | /// struct PhotoLibraryManager { 42 | /// func accessPhotoLibrary() throws(PermissionError) { 43 | /// guard isPhotoLibraryAccessAllowed() else { 44 | /// throw .denied(permission: "Photo Library") 45 | /// } 46 | /// // Photo library access logic 47 | /// } 48 | /// } 49 | /// ``` 50 | /// - Parameter permission: The type of permission that was denied. 51 | case denied(permission: String) 52 | 53 | /// The app lacks a required permission and the user cannot grant it. 54 | /// 55 | /// # Example 56 | /// ```swift 57 | /// struct HealthDataService { 58 | /// func accessHealthData() throws(PermissionError) { 59 | /// guard canRequestHealthPermission() else { 60 | /// throw .restricted(permission: "Health Data") 61 | /// } 62 | /// // Health data access logic 63 | /// } 64 | /// } 65 | /// ``` 66 | /// - Parameter permission: The type of permission required. 67 | case restricted(permission: String) 68 | 69 | /// The app lacks a required permission, and it is unknown whether the user can grant it. 70 | /// 71 | /// # Example 72 | /// ```swift 73 | /// struct NotificationManager { 74 | /// func setupNotifications() throws(PermissionError) { 75 | /// guard isNotificationPermissionStatusKnown() else { 76 | /// throw .notDetermined(permission: "Notifications") 77 | /// } 78 | /// // Notification setup logic 79 | /// } 80 | /// } 81 | /// ``` 82 | case notDetermined(permission: String) 83 | 84 | /// Generic error message if the existing cases don't provide the required details. 85 | /// 86 | /// # Example 87 | /// ```swift 88 | /// struct UnexpectedPermissionHandler { 89 | /// func handleSpecialCase() throws(PermissionError) { 90 | /// guard !isHandledCase() else { 91 | /// throw .generic(userFriendlyMessage: "A unique permission error occurred") 92 | /// } 93 | /// // Special case handling 94 | /// } 95 | /// } 96 | /// ``` 97 | case generic(userFriendlyMessage: String) 98 | 99 | /// An error that occurred during permission handling, wrapped into this error type using the ``catch(_:)`` function. 100 | /// This could include authorization errors, system permission errors, or any other errors encountered during permission requests. 101 | /// 102 | /// # Example 103 | /// ```swift 104 | /// struct MediaAccessManager { 105 | /// func requestMediaAccess() throws(PermissionError) { 106 | /// // Regular error for denied permission 107 | /// guard !isPermissionExplicitlyDenied else { 108 | /// throw PermissionError.denied(permission: "Media Library") 109 | /// } 110 | /// 111 | /// // Automatically wrap authorization and system permission errors 112 | /// try PermissionError.catch { 113 | /// try await AVCaptureDevice.requestAccess(for: .video) 114 | /// try await PHPhotoLibrary.requestAuthorization(for: .readWrite) 115 | /// } 116 | /// } 117 | /// } 118 | /// ``` 119 | /// 120 | /// The `caught` case stores the original error while maintaining type safety through typed throws. 121 | /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function 122 | /// which automatically wraps any thrown errors into this case. 123 | /// 124 | /// - Parameters: 125 | /// - error: The original error that occurred during the permission operation. 126 | case caught(Error) 127 | 128 | /// A user-friendly error message suitable for display to end users. 129 | public var userFriendlyMessage: String { 130 | switch self { 131 | case .denied(let permission): 132 | return String.localized( 133 | key: "BuiltInErrors.PermissionError.denied", 134 | defaultValue: "Access to \(permission) was declined. To use this feature, please enable the permission in your device Settings." 135 | ) 136 | case .restricted(let permission): 137 | return String.localized( 138 | key: "BuiltInErrors.PermissionError.restricted", 139 | defaultValue: "Access to \(permission) is currently restricted. This may be due to system settings or parental controls." 140 | ) 141 | case .notDetermined(let permission): 142 | return String.localized( 143 | key: "BuiltInErrors.PermissionError.notDetermined", 144 | defaultValue: "Permission for \(permission) has not been confirmed. Please review and grant access in your device Settings." 145 | ) 146 | case .generic(let userFriendlyMessage): 147 | return userFriendlyMessage 148 | case .caught(let error): 149 | return ErrorKit.userFriendlyMessage(for: error) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/ErrorKit/BuiltInErrors/StateError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents errors caused by invalid or unexpected states. 4 | /// 5 | /// # Examples of Use 6 | /// 7 | /// ## Managing State Transitions 8 | /// ```swift 9 | /// struct OrderProcessor { 10 | /// func processOrder(_ order: Order) throws(StateError) { 11 | /// guard order.status == .pending else { 12 | /// throw .invalidState(description: "Order must be in pending state") 13 | /// } 14 | /// // Order processing logic 15 | /// } 16 | /// } 17 | /// ``` 18 | /// 19 | /// ## Handling Finalized States 20 | /// ```swift 21 | /// struct DocumentManager { 22 | /// func updateDocument(_ doc: Document) throws(StateError) { 23 | /// guard !doc.isFinalized else { 24 | /// throw .alreadyFinalized 25 | /// } 26 | /// // Document update logic 27 | /// } 28 | /// } 29 | /// ``` 30 | public enum StateError: Throwable, Catching { 31 | /// The required state was not met to proceed with the operation. 32 | /// 33 | /// # Example 34 | /// ```swift 35 | /// struct PaymentProcessor { 36 | /// func refundPayment(_ payment: Payment) throws(StateError) { 37 | /// guard payment.status == .completed else { 38 | /// throw .invalidState(description: "Payment must be completed") 39 | /// } 40 | /// // Refund processing logic 41 | /// } 42 | /// } 43 | /// ``` 44 | case invalidState(description: String) 45 | 46 | /// The operation cannot proceed because the state has already been finalized. 47 | /// 48 | /// # Example 49 | /// ```swift 50 | /// struct ContractManager { 51 | /// func modifyContract(_ contract: Contract) throws(StateError) { 52 | /// guard !contract.isFinalized else { 53 | /// throw .alreadyFinalized 54 | /// } 55 | /// // Contract modification logic 56 | /// } 57 | /// } 58 | /// ``` 59 | case alreadyFinalized 60 | 61 | /// A required precondition for the operation was not met. 62 | /// 63 | /// # Example 64 | /// ```swift 65 | /// struct GameEngine { 66 | /// func startNewLevel() throws(StateError) { 67 | /// guard player.hasCompletedTutorial else { 68 | /// throw .preconditionFailed(description: "Tutorial must be completed") 69 | /// } 70 | /// // Level initialization logic 71 | /// } 72 | /// } 73 | /// ``` 74 | case preconditionFailed(description: String) 75 | 76 | /// Generic error message if the existing cases don't provide the required details. 77 | /// 78 | /// # Example 79 | /// ```swift 80 | /// struct StateHandler { 81 | /// func handleUnexpectedState() throws(StateError) { 82 | /// guard isStateValid() else { 83 | /// throw .generic(userFriendlyMessage: "System is in an unexpected state") 84 | /// } 85 | /// // State handling logic 86 | /// } 87 | /// } 88 | /// ``` 89 | case generic(userFriendlyMessage: String) 90 | 91 | /// An error that occurred due to state management issues, wrapped into this error type using the ``catch(_:)`` function. 92 | /// This could include state transition errors, validation errors, or any other errors encountered during state-dependent operations. 93 | /// 94 | /// # Example 95 | /// ```swift 96 | /// struct OrderProcessor { 97 | /// func finalizeOrder(_ order: Order) throws(StateError) { 98 | /// // Regular error for invalid state 99 | /// guard order.status == .verified else { 100 | /// throw StateError.invalidState(description: "Order must be verified") 101 | /// } 102 | /// 103 | /// // Automatically wrap payment and inventory state errors 104 | /// try StateError.catch { 105 | /// let paymentResult = try paymentGateway.processPayment(order.payment) 106 | /// try inventoryManager.reserveItems(order.items) 107 | /// try order.moveToState(.finalized) 108 | /// } 109 | /// } 110 | /// } 111 | /// ``` 112 | /// 113 | /// The `caught` case stores the original error while maintaining type safety through typed throws. 114 | /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function 115 | /// which automatically wraps any thrown errors into this case. 116 | /// 117 | /// - Parameters: 118 | /// - error: The original error that occurred during the state-dependent operation. 119 | case caught(Error) 120 | 121 | /// A user-friendly error message suitable for display to end users. 122 | public var userFriendlyMessage: String { 123 | switch self { 124 | case .invalidState(let description): 125 | return String.localized( 126 | key: "BuiltInErrors.StateError.invalidState", 127 | defaultValue: "The current state prevents this action: \(description). Please ensure all requirements are met and try again." 128 | ) 129 | case .alreadyFinalized: 130 | return String.localized( 131 | key: "BuiltInErrors.StateError.alreadyFinalized", 132 | defaultValue: "This item has already been finalized and cannot be modified. Please create a new version if changes are needed." 133 | ) 134 | case .preconditionFailed(let description): 135 | return String.localized( 136 | key: "BuiltInErrors.StateError.preconditionFailed", 137 | defaultValue: "A required condition was not met: \(description). Please complete all prerequisites before proceeding." 138 | ) 139 | case .generic(let userFriendlyMessage): 140 | return userFriendlyMessage 141 | case .caught(let error): 142 | return ErrorKit.userFriendlyMessage(for: error) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Sources/ErrorKit/BuiltInErrors/ValidationError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents errors related to validation failures. 4 | /// 5 | /// # Examples of Use 6 | /// 7 | /// ## Validating Form Input 8 | /// ```swift 9 | /// struct UserRegistrationValidator { 10 | /// func validateUsername(_ username: String) throws(ValidationError) { 11 | /// guard !username.isEmpty else { 12 | /// throw .missingField(field: "Username") 13 | /// } 14 | /// 15 | /// guard username.count <= 30 else { 16 | /// throw .inputTooLong(field: "Username", maxLength: 30) 17 | /// } 18 | /// 19 | /// guard isValidUsername(username) else { 20 | /// throw .invalidInput(field: "Username") 21 | /// } 22 | /// } 23 | /// } 24 | /// ``` 25 | /// 26 | /// ## Handling Required Fields 27 | /// ```swift 28 | /// struct PaymentFormValidator { 29 | /// func validatePaymentDetails(_ details: [String: String]) throws(ValidationError) { 30 | /// guard let cardNumber = details["cardNumber"], !cardNumber.isEmpty else { 31 | /// throw .missingField(field: "Card Number") 32 | /// } 33 | /// // Additional validation logic 34 | /// } 35 | /// } 36 | /// ``` 37 | public enum ValidationError: Throwable, Catching { 38 | /// The input provided is invalid. 39 | /// 40 | /// # Example 41 | /// ```swift 42 | /// struct EmailValidator { 43 | /// func validateEmail(_ email: String) throws(ValidationError) { 44 | /// guard isValidEmailFormat(email) else { 45 | /// throw .invalidInput(field: "Email Address") 46 | /// } 47 | /// // Additional email validation 48 | /// } 49 | /// } 50 | /// ``` 51 | /// - Parameters: 52 | /// - field: The name of the field that caused the error. 53 | case invalidInput(field: String) 54 | 55 | /// A required field is missing. 56 | /// 57 | /// # Example 58 | /// ```swift 59 | /// struct ShippingAddressValidator { 60 | /// func validateAddress(_ address: Address) throws(ValidationError) { 61 | /// guard !address.street.isEmpty else { 62 | /// throw .missingField(field: "Street Address") 63 | /// } 64 | /// // Additional address validation 65 | /// } 66 | /// } 67 | /// ``` 68 | /// - Parameters: 69 | /// - field: The name of the required fields. 70 | case missingField(field: String) 71 | 72 | /// The input exceeds the maximum allowed length. 73 | /// 74 | /// # Example 75 | /// ```swift 76 | /// struct CommentValidator { 77 | /// func validateComment(_ text: String) throws(ValidationError) { 78 | /// guard text.count <= 1000 else { 79 | /// throw .inputTooLong(field: "Comment", maxLength: 1000) 80 | /// } 81 | /// // Additional comment validation 82 | /// } 83 | /// } 84 | /// ``` 85 | /// - Parameters: 86 | /// - field: The name of the field that caused the error. 87 | /// - maxLength: The maximum allowed length for the field. 88 | case inputTooLong(field: String, maxLength: Int) 89 | 90 | /// Generic error message if the existing cases don't provide the required details. 91 | /// 92 | /// # Example 93 | /// ```swift 94 | /// struct CustomValidator { 95 | /// func validateSpecialCase(_ input: String) throws(ValidationError) { 96 | /// guard meetsCustomRequirements(input) else { 97 | /// throw .generic(userFriendlyMessage: "Input does not meet requirements") 98 | /// } 99 | /// // Special validation logic 100 | /// } 101 | /// } 102 | /// ``` 103 | case generic(userFriendlyMessage: String) 104 | 105 | /// An error that occurred during validation, wrapped into this error type using the ``catch(_:)`` function. 106 | /// This could include data validation errors, format validation errors, or any other errors encountered during validation checks. 107 | /// 108 | /// # Example 109 | /// ```swift 110 | /// struct UserProfileValidator { 111 | /// func validateProfile(_ profile: UserProfile) throws(ValidationError) { 112 | /// // Regular error for field validation 113 | /// guard !profile.name.isEmpty else { 114 | /// throw ValidationError.missingField(field: "Name") 115 | /// } 116 | /// 117 | /// // Automatically wrap complex validation errors 118 | /// try ValidationError.catch { 119 | /// try emailValidator.validateEmailFormat(profile.email) 120 | /// try addressValidator.validateAddress(profile.address) 121 | /// try customFieldsValidator.validate(profile.customFields) 122 | /// } 123 | /// } 124 | /// } 125 | /// ``` 126 | /// 127 | /// The `caught` case stores the original error while maintaining type safety through typed throws. 128 | /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function 129 | /// which automatically wraps any thrown errors into this case. 130 | /// 131 | /// - Parameters: 132 | /// - error: The original error that occurred during the validation operation. 133 | case caught(Error) 134 | 135 | /// A user-friendly error message suitable for display to end users. 136 | public var userFriendlyMessage: String { 137 | switch self { 138 | case .invalidInput(let field): 139 | return String.localized( 140 | key: "BuiltInErrors.ValidationError.invalidInput", 141 | defaultValue: "The value entered for \(field) is not in the correct format. Please review the requirements and try again." 142 | ) 143 | case .missingField(let field): 144 | return String.localized( 145 | key: "BuiltInErrors.ValidationError.missingField", 146 | defaultValue: "Please provide a value for \(field). This information is required to proceed." 147 | ) 148 | case .inputTooLong(let field, let maxLength): 149 | return String.localized( 150 | key: "BuiltInErrors.ValidationError.inputTooLong", 151 | defaultValue: "The \(field) field cannot be longer than \(maxLength) characters. Please shorten your input and try again." 152 | ) 153 | case .generic(let userFriendlyMessage): 154 | return userFriendlyMessage 155 | case .caught(let error): 156 | return ErrorKit.userFriendlyMessage(for: error) 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/ErrorKit/Catching.swift: -------------------------------------------------------------------------------- 1 | /// A protocol built for typed throws that enables automatic error wrapping for nested error hierarchies through a `caught` case. 2 | /// This simplifies error handling in modular applications where errors need to be propagated up through multiple layers. 3 | /// 4 | /// # Overview 5 | /// When working with nested error types in a modular application, you often need to wrap errors from lower-level 6 | /// modules into higher-level error types. This protocol provides a convenient way to handle such error wrapping 7 | /// without manually defining wrapper cases for each possible error type. 8 | /// 9 | /// # Example 10 | /// Consider an app with profile management that uses both database and file operations: 11 | /// ```swift 12 | /// // Lower-level error types 13 | /// enum DatabaseError: Throwable, Catching { 14 | /// case connectionFailed 15 | /// case recordNotFound(entity: String, identifier: String?) 16 | /// case caught(Error) // Wraps any other database-related errors 17 | /// } 18 | /// 19 | /// enum FileError: Throwable, Catching { 20 | /// case notFound(path: String) 21 | /// case accessDenied(path: String) 22 | /// case caught(Error) // Wraps any other file system errors 23 | /// } 24 | /// 25 | /// // Higher-level error type 26 | /// enum ProfileError: Throwable, Catching { 27 | /// case validationFailed(field: String) 28 | /// case caught(Error) // Automatically wraps both DatabaseError and FileError 29 | /// } 30 | /// 31 | /// struct ProfileRepository { 32 | /// func loadProfile(id: String) throws(ProfileError) { 33 | /// // Explicit error for validation 34 | /// guard id.isValidFormat else { 35 | /// throw ProfileError.validationFailed(field: "id") 36 | /// } 37 | /// 38 | /// // Automatically wrap any database or file errors 39 | /// let userData = try ProfileError.catch { 40 | /// let user = try database.loadUser(id) 41 | /// let settings = try fileSystem.readUserSettings(user.settingsPath) 42 | /// return UserProfile(user: user, settings: settings) 43 | /// } 44 | /// 45 | /// // Use the loaded data 46 | /// self.currentProfile = userData 47 | /// } 48 | /// } 49 | /// ``` 50 | /// 51 | /// Without Catching protocol, you would need explicit cases and manual mapping: 52 | /// ```swift 53 | /// enum ProfileError: Throwable { 54 | /// case validationFailed(field: String) 55 | /// case databaseError(DatabaseError) // Extra case needed 56 | /// case fileError(FileError) // Extra case needed 57 | /// } 58 | /// 59 | /// struct ProfileRepository { 60 | /// func loadProfile(id: String) throws(ProfileError) { 61 | /// guard id.isValidFormat else { 62 | /// throw ProfileError.validationFailed(field: "id") 63 | /// } 64 | /// 65 | /// // Manual error mapping needed for each error type 66 | /// do { 67 | /// let user = try database.loadUser(id) 68 | /// // Nested try-catch needed 69 | /// do { 70 | /// let settings = try fileSystem.readUserSettings(user.settingsPath) 71 | /// self.currentProfile = UserProfile(user: user, settings: settings) 72 | /// } catch let error as FileError { 73 | /// throw .fileError(error) 74 | /// } 75 | /// } catch let error as DatabaseError { 76 | /// throw .databaseError(error) 77 | /// } 78 | /// } 79 | /// } 80 | /// ``` 81 | /// 82 | /// # Benefits 83 | /// - Simplified error type definitions with a single catch-all case 84 | /// - Automatic wrapping of any error type without manual case mapping 85 | /// - Maintained type safety through typed throws 86 | /// - Clean, readable error handling code 87 | /// - Easy propagation of errors through multiple layers 88 | /// - Transparent handling of return values from wrapped operations 89 | public protocol Catching { 90 | /// Creates an instance of this error type that wraps another error. 91 | /// Used internally by the ``catch(_:)`` function to automatically wrap any thrown errors. 92 | /// 93 | /// - Parameter error: The error to be wrapped in this error type. 94 | static func caught(_ error: Error) -> Self 95 | } 96 | 97 | extension Catching { 98 | /// Executes a throwing operation and automatically wraps any thrown errors into this error type's `caught` case, 99 | /// while passing through the operation's return value on success. Great for functions using typed throws. 100 | /// 101 | /// # Overview 102 | /// This function provides a convenient way to: 103 | /// - Execute throwing operations 104 | /// - Automatically wrap any errors into the current error type 105 | /// - Pass through return values from the wrapped code 106 | /// - Maintain type safety with typed throws 107 | /// 108 | /// # Example 109 | /// ```swift 110 | /// struct ProfileRepository { 111 | /// func loadProfile(id: String) throws(ProfileError) { 112 | /// // Regular error throwing for validation 113 | /// guard id.isValidFormat else { 114 | /// throw ProfileError.validationFailed(field: "id") 115 | /// } 116 | /// 117 | /// // Automatically wrap any database or file errors while handling return value 118 | /// let userData = try ProfileError.catch { 119 | /// let user = try database.loadUser(id) 120 | /// let settings = try fileSystem.readUserSettings(user.settingsPath) 121 | /// return UserProfile(user: user, settings: settings) 122 | /// } 123 | /// 124 | /// // Use the loaded data 125 | /// self.currentProfile = userData 126 | /// } 127 | /// } 128 | /// ``` 129 | /// 130 | /// - Parameter operation: The throwing operation to execute. 131 | /// - Returns: The value returned by the operation if successful. 132 | /// - Throws: An instance of `Self` with the original error wrapped in the `caught` case. 133 | public static func `catch`( 134 | _ operation: () throws -> ReturnType 135 | ) throws(Self) -> ReturnType { 136 | do { 137 | return try operation() 138 | } catch let error as Self { // Avoid nesting if the error is already of the expected type 139 | throw error 140 | } catch { 141 | throw Self.caught(error) 142 | } 143 | } 144 | 145 | /// Executes an async throwing operation and automatically wraps any thrown errors into this error type's `caught` case, 146 | /// while passing through the operation's return value on success. Great for functions using typed throws. 147 | /// 148 | /// # Overview 149 | /// This function provides a convenient way to: 150 | /// - Execute async throwing operations 151 | /// - Automatically wrap any errors into the current error type 152 | /// - Pass through return values from the wrapped code 153 | /// - Maintain type safety with typed throws 154 | /// 155 | /// # Example 156 | /// ```swift 157 | /// struct ProfileRepository { 158 | /// func loadProfile(id: String) throws(ProfileError) { 159 | /// // Regular error throwing for validation 160 | /// guard id.isValidFormat else { 161 | /// throw ProfileError.validationFailed(field: "id") 162 | /// } 163 | /// 164 | /// // Automatically wrap any database or file errors while handling return value 165 | /// let userData = try await ProfileError.catch { 166 | /// let user = try await database.loadUser(id) 167 | /// let settings = try await fileSystem.readUserSettings(user.settingsPath) 168 | /// return UserProfile(user: user, settings: settings) 169 | /// } 170 | /// 171 | /// // Use the loaded data 172 | /// self.currentProfile = userData 173 | /// } 174 | /// } 175 | /// ``` 176 | /// 177 | /// - Parameter operation: The async throwing operation to execute. 178 | /// - Returns: The value returned by the operation if successful. 179 | /// - Throws: An instance of `Self` with the original error wrapped in the `caught` case. 180 | public static func `catch`( 181 | _ operation: () async throws -> ReturnType 182 | ) async throws(Self) -> ReturnType { 183 | do { 184 | return try await operation() 185 | } catch let error as Self { // Avoid nesting if the error is already of the expected type 186 | throw error 187 | } catch { 188 | throw Self.caught(error) 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/ErrorKit.md: -------------------------------------------------------------------------------- 1 | # ``ErrorKit`` 2 | 3 | Making error handling in Swift more intuitive and powerful with clearer messages, type safety, and user-friendly diagnostics. 4 | 5 | @Metadata { 6 | @PageImage(purpose: icon, source: "ErrorKit") 7 | } 8 | 9 | ## Overview 10 | 11 | Swift's error handling has several limitations that make it challenging to create robust, user-friendly applications: 12 | - The `Error` protocol's confusing behavior with `localizedDescription` 13 | - Hard-to-understand system error messages 14 | - Limited type safety in error propagation 15 | - Difficulties with error chain debugging 16 | - Challenges in collecting meaningful feedback from users 17 | 18 | ErrorKit addresses these challenges with a suite of lightweight, interconnected features you can adopt progressively. 19 | 20 | ## Core Features 21 | 22 | These foundational features improve how you define and present errors: 23 | 24 | @Links(visualStyle: detailedGrid) { 25 | - 26 | - 27 | } 28 | 29 | ## Swift 6 Typed Throws Support 30 | 31 | Swift 6 introduces typed throws (`throws(ErrorType)`), bringing compile-time type checking to error handling. ErrorKit makes this powerful feature practical with solutions for its biggest challenges: 32 | 33 | @Links(visualStyle: detailedGrid) { 34 | - 35 | - 36 | } 37 | 38 | ## Ready-to-Use Tools 39 | 40 | These practical tools help you implement robust error handling with minimal effort: 41 | 42 | @Links(visualStyle: detailedGrid) { 43 | - 44 | - 45 | } 46 | 47 | ## How These Features Work Together 48 | 49 | ErrorKit's features are designed to complement each other while remaining independently useful: 50 | 51 | 1. **Start with improved error definitions** using `Throwable` for custom errors and `userFriendlyMessage(for:)` for system errors. 52 | 53 | 2. **Add type safety with Swift 6 typed throws**, using the `Catching` protocol to solve nested error challenges. This pairs with error chain debugging to understand error flows through your app. 54 | 55 | 3. **Save time with ready-made tools**: built-in error types for common scenarios and simple log collection for user feedback. 56 | 57 | Each feature builds upon the foundations laid by the previous ones, but you can adopt any part independently based on your needs. 58 | 59 | ## Adoption Path 60 | 61 | Here's a practical adoption strategy: 62 | 63 | 1. Replace `Error` with `Throwable` in your custom error types 64 | 2. Use `ErrorKit.userFriendlyMessage(for:)` when showing system errors 65 | 3. Adopt built-in error types where they fit your needs 66 | 4. Implement typed throws with `Catching` for more robust error flows 67 | 5. Add error chain debugging to improve error visibility 68 | 6. Integrate log collection with your feedback system 69 | -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Guides/Enhanced-Error-Descriptions.md: -------------------------------------------------------------------------------- 1 | # Enhanced Error Descriptions 2 | 3 | Transform cryptic system errors into clear, actionable messages with better descriptions. 4 | 5 | @Metadata { 6 | @PageImage(purpose: icon, source: "ErrorKit") 7 | @PageImage(purpose: card, source: "EnhancedDescriptions") 8 | } 9 | 10 | ## Highlights 11 | 12 | System errors from Apple frameworks often return messages that are too technical or vague to be helpful. ErrorKit provides enhanced, user-friendly descriptions for these errors through the `userFriendlyMessage(for:)` function. 13 | 14 | ### The Problem with System Error Messages 15 | 16 | When working with system APIs, errors often have unclear or technical messages that may confuse users or fail to provide actionable information about how to resolve the issue. 17 | 18 | ### The Solution: Enhanced Error Descriptions 19 | 20 | ErrorKit's `userFriendlyMessage(for:)` function provides improved error messages: 21 | 22 | ```swift 23 | do { 24 | let url = URL(string: "https://example.com")! 25 | let _ = try Data(contentsOf: url) 26 | } catch { 27 | // Instead of the default error message, get a more user-friendly one 28 | print(ErrorKit.userFriendlyMessage(for: error)) 29 | } 30 | ``` 31 | 32 | This function works with any error type, including system errors and your own custom errors. It maintains all the benefits of your custom `Throwable` types while enhancing system errors with more helpful messages. 33 | 34 | ### Error Domain Coverage 35 | 36 | ErrorKit provides enhanced messages for errors from several system frameworks and domains: 37 | 38 | #### Foundation Domain 39 | - Network errors (URLError) 40 | - Connection issues 41 | - Timeouts 42 | - Host not found 43 | - File operations (CocoaError) 44 | - File not found 45 | - Permission issues 46 | - Disk space errors 47 | - System errors (POSIXError) 48 | - Disk space 49 | - Access permission 50 | - File descriptor issues 51 | 52 | #### CoreData Domain 53 | - Store save errors 54 | - Validation errors 55 | - Relationship errors 56 | - Store compatibility issues 57 | - Model validation errors 58 | 59 | #### MapKit Domain 60 | - Server failures 61 | - Throttling errors 62 | - Placemark not found 63 | - Direction finding failures 64 | 65 | ### Works Seamlessly with Throwable 66 | 67 | The `userFriendlyMessage(for:)` function integrates perfectly with ErrorKit's `Throwable` protocol: 68 | 69 | ```swift 70 | do { 71 | try riskyOperation() 72 | } catch { 73 | // Works with both custom Throwable errors and system errors 74 | showAlert(message: ErrorKit.userFriendlyMessage(for: error)) 75 | } 76 | ``` 77 | 78 | If the error already conforms to `Throwable`, its `userFriendlyMessage` is used. For system errors, ErrorKit provides an enhanced description from its built-in mappers. 79 | 80 | ### Localization Support 81 | 82 | All enhanced error messages are fully localized using the `String(localized:)` pattern, ensuring users receive messages in their preferred language where available. 83 | 84 | ### String Interpolation Convenience 85 | 86 | ErrorKit enhances Swift's string interpolation to automatically use `userFriendlyMessage(for:)`: 87 | 88 | ```swift 89 | // Instead of: 90 | showAlert(message: "Save failed: \(ErrorKit.userFriendlyMessage(for: error))") 91 | 92 | // You can simply use: 93 | showAlert(message: "Save failed: \(error)") 94 | Text("Could not load data: \(error)") 95 | Logger().info("Sync completed with error: \(error)") 96 | ``` 97 | 98 | This works with any error type — both your custom `Throwable` errors and system errors. 99 | 100 | ### How It Works 101 | 102 | The `userFriendlyMessage(for:)` function follows this process to determine the best error message: 103 | 104 | 1. If the error conforms to `Throwable`, it uses the error's own `userFriendlyMessage` 105 | 2. It queries registered error mappers to find enhanced descriptions 106 | 3. If the error conforms to `LocalizedError`, it combines its localized properties 107 | 4. As a fallback, it formats the NSError domain and code along with the standard `localizedDescription` 108 | 109 | ### Contributing New Descriptions 110 | 111 | You can help improve ErrorKit by contributing better error descriptions for common error types: 112 | 113 | 1. Identify cryptic error messages from system frameworks 114 | 2. Implement domain-specific handlers or extend existing ones (see folder `ErrorMappers`) 115 | 3. Use clear, actionable language that helps users understand what went wrong 116 | 4. Include localization support for all messages (no need to actually localize, we'll take care) 117 | 118 | Example contribution to handle a new error type: 119 | 120 | ```swift 121 | // In FoundationErrorMapper.swift 122 | case let jsonError as NSError where jsonError.domain == NSCocoaErrorDomain && jsonError.code == 3840: 123 | return String(localized: "The data couldn't be read because it isn't in the correct format.") 124 | ``` 125 | 126 | ### Custom Error Mappers 127 | 128 | While ErrorKit focuses on enhancing system and framework errors, you can also create custom mappers for any library: 129 | 130 | ```swift 131 | enum MyLibraryErrorMapper: ErrorMapper { 132 | static func userFriendlyMessage(for error: Error) -> String? { 133 | switch error { 134 | case let libraryError as MyLibrary.Error: 135 | switch libraryError { 136 | case .apiKeyExpired: 137 | return String(localized: "API key expired. Please update your credentials.") 138 | default: 139 | return nil 140 | } 141 | default: 142 | return nil 143 | } 144 | } 145 | } 146 | 147 | // On app start: 148 | ErrorKit.registerMapper(MyLibraryErrorMapper.self) 149 | ``` 150 | 151 | This extensibility allows the community to create mappers for 3rd-party libraries with known error issues. 152 | 153 | ## Topics 154 | 155 | ### Essentials 156 | 157 | - ``ErrorKit/userFriendlyMessage(for:)`` 158 | - ``ErrorMapper`` 159 | 160 | ### Built-in Mappers 161 | 162 | - ``FoundationErrorMapper`` 163 | - ``CoreDataErrorMapper`` 164 | - ``MapKitErrorMapper`` 165 | 166 | ### Continue Reading 167 | 168 | - 169 | -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Guides/Error-Chain-Debugging.md: -------------------------------------------------------------------------------- 1 | # Error Chain Debugging 2 | 3 | Trace the complete path of errors through your application with rich hierarchical debugging. 4 | 5 | @Metadata { 6 | @PageImage(purpose: icon, source: "ErrorKit") 7 | @PageImage(purpose: card, source: "ErrorChainDebugging") 8 | } 9 | 10 | ## Highlights 11 | 12 | One of the most challenging aspects of error handling in Swift is understanding exactly where an error originated, especially when using error wrapping across multiple layers of an application. ErrorKit solves this with powerful debugging tools that help you understand the complete error chain. 13 | 14 | ### The Problem with Traditional Error Logging 15 | 16 | When logging errors in Swift, you typically lose context about how an error propagated through your application: 17 | 18 | ```swift 19 | do { 20 | try await updateUserProfile() 21 | } catch { 22 | // 😕 Only shows the leaf error with no chain information 23 | Logger().error("Error occurred: \(error)") 24 | 25 | // 😕 Shows a better message but still no error chain 26 | Logger().error("Error: \(ErrorKit.userFriendlyMessage(for: error))") 27 | // Output: "Could not find database file." 28 | } 29 | ``` 30 | 31 | This makes it difficult to: 32 | - Understand which module or layer originally threw the error 33 | - Trace the error's path through your application 34 | - Group similar errors for analysis 35 | - Prioritize which errors to fix first 36 | 37 | ### Solution: Error Chain Description 38 | 39 | ErrorKit's `errorChainDescription(for:)` function provides a comprehensive view of the entire error chain, showing you exactly how an error propagated through your application: 40 | 41 | ```swift 42 | do { 43 | try await updateUserProfile() 44 | } catch { 45 | // 🎯 Always use this for debug logging 46 | Logger().error("\(ErrorKit.errorChainDescription(for: error))") 47 | 48 | // Output shows the complete chain: 49 | // ProfileError 50 | // └─ DatabaseError 51 | // └─ FileError.notFound(path: "/Users/data.db") 52 | // └─ userFriendlyMessage: "Could not find database file." 53 | } 54 | ``` 55 | 56 | This hierarchical view tells you: 57 | 1. Where the error originated (FileError) 58 | 2. How it was wrapped (DatabaseError → ProfileError) 59 | 3. What exactly went wrong (file not found) 60 | 4. The user-friendly message (reported to users) 61 | 62 | For errors conforming to the `Catching` protocol, you get the complete error wrapping chain. This is why it's important for your own error types and any Swift packages you develop to adopt both `Throwable` and `Catching` - it not only makes them work better with typed throws but also enables automatic extraction of the full error chain. 63 | 64 | Even for errors that don't conform to `Catching`, you still get valuable information since most Swift errors are enums. The error chain description will show you the exact enum case (e.g., `FileError.notFound`), making it easy to search your codebase for the error's origin. This is much better than the default cryptic message you get for enum cases when using `localizedDescription`. 65 | 66 | ### String Interpolation for Debug Logging 67 | 68 | ErrorKit enhances string interpolation for error chain debugging. Use either `chain:` or `debug:` (they're aliases) to get the complete hierarchical description: 69 | 70 | ```swift 71 | // Instead of: 72 | print("Update failed:\n\(ErrorKit.errorChainDescription(for: error))") 73 | 74 | // You can simply use either: 75 | print("Update failed:\n\(chain: error)") 76 | print("Update failed:\n\(debug: error)") 77 | ``` 78 | 79 | Use `\(error)` for user-facing messages, `\(chain: error)` or `\(debug: error)` for debugging. 80 | 81 | For `OSLog`/`Logger` there are dedicated convenience overloads taking a second `error` parameter: 82 | 83 | ```swift 84 | // Instead of: 85 | Logger().error("Update failed:\n\(ErrorKit.errorChainDescription(for: error))") 86 | 87 | // You can simply use overloads like: 88 | Logger().error("Update failed", error: error) 89 | Logger().warning("Update failed", error: error) 90 | ``` 91 | 92 | There's no need to add a colon (`:`) or newline (`\n`) to the message, they will be added automatically. 93 | 94 | ### Error Analytics with Grouping IDs 95 | 96 | To help prioritize which errors to fix, ErrorKit provides `groupingID(for:)` that generates stable identifiers for errors sharing the exact same type structure and enum cases: 97 | 98 | ```swift 99 | struct ErrorTracker { 100 | static func log(_ error: Error) { 101 | // Get a stable ID that ignores dynamic parameters 102 | let groupID = ErrorKit.groupingID(for: error) // e.g. "3f9d2a" 103 | 104 | Analytics.track( 105 | event: "error_occurred", 106 | properties: [ 107 | "error_group": groupID, 108 | "error_details": ErrorKit.errorChainDescription(for: error) 109 | ] 110 | ) 111 | } 112 | } 113 | ``` 114 | 115 | The grouping ID generates the same identifier for errors that have identical: 116 | - Error type hierarchy 117 | - Enum cases in the chain 118 | 119 | But it ignores: 120 | - Dynamic parameters (file paths, field names, etc.) 121 | - User-friendly messages (which might be localized or dynamic) 122 | 123 | For example, these errors have the same grouping ID since they differ only in their dynamic path parameters: 124 | ```swift 125 | // Both generate groupID: "3f9d2a" 126 | ProfileError 127 | └─ DatabaseError 128 | └─ FileError.notFound(path: "/Users/john/data.db") 129 | └─ userFriendlyMessage: "Could not find database file." 130 | 131 | ProfileError 132 | └─ DatabaseError 133 | └─ FileError.notFound(path: "/Users/jane/backup.db") 134 | └─ userFriendlyMessage: "Die Backup-Datenbank konnte nicht gefunden werden." 135 | ``` 136 | 137 | This precise grouping allows you to track true error frequencies in analytics, create meaningful charts of common error patterns, and prioritize which errors to fix first. 138 | 139 | ### Implementation and Integration 140 | 141 | Under the hood, `errorChainDescription(for:)` uses Swift's reflection to examine error objects, recursively traversing the error chain and formatting everything in a hierarchical tree structure with the user-friendly message at each leaf node. 142 | 143 | To integrate with your logging system: 144 | 145 | ```swift 146 | extension Logger { 147 | func logError(_ error: Error, file: String = #file, function: String = #function, line: Int = #line) { 148 | let errorChain = ErrorKit.errorChainDescription(for: error) 149 | self.error("\(errorChain, privacy: .public)", file: file, function: function, line: line) 150 | } 151 | } 152 | ``` 153 | 154 | For crash reporting and analytics, include both the error chain and grouping ID: 155 | 156 | ```swift 157 | func reportCrash(_ error: Error) { 158 | CrashReporting.send( 159 | error: error, 160 | metadata: [ 161 | "errorChain": ErrorKit.errorChainDescription(for: error), 162 | "errorGroup": ErrorKit.groupingID(for: error) 163 | ] 164 | ) 165 | } 166 | ``` 167 | 168 | ### Best Practices 169 | 170 | To get the most out of error chain debugging: 171 | 172 | 1. **Use `Catching` consistently**: Add `Catching` conformance to all your error types that might wrap other errors. 173 | 2. **Include error chain descriptions in logs**: Always use `errorChainDescription(for:)` when logging errors. 174 | 3. **Group errors for analytics**: Use `groupingID(for:)` to track error frequencies. 175 | 176 | ### Summary 177 | 178 | ErrorKit's debugging tools transform error handling from a black box into a transparent system. By combining `errorChainDescription` for debugging with `groupingID` for analytics, you get deep insight into error flows while maintaining the ability to track and prioritize issues effectively. This is particularly powerful when combined with ErrorKit's `Catching` protocol, creating a comprehensive system for error handling, debugging, and monitoring. 179 | 180 | ## Topics 181 | 182 | ### Essentials 183 | 184 | - ``ErrorKit/errorChainDescription(for:)`` 185 | - ``ErrorKit/groupingID(for:)`` 186 | 187 | ### Related Concepts 188 | 189 | - ``Catching`` 190 | - ``Throwable`` 191 | 192 | ### Continue Reading 193 | 194 | - 195 | -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Guides/Throwable-Protocol.md: -------------------------------------------------------------------------------- 1 | # Throwable Protocol 2 | 3 | Making error messages work as expected in Swift with a more intuitive protocol. 4 | 5 | @Metadata { 6 | @PageImage(purpose: icon, source: "ErrorKit") 7 | @PageImage(purpose: card, source: "ThrowableProtocol") 8 | } 9 | 10 | ## Highlights 11 | 12 | Swift's built-in `Error` protocol has a confusing quirk: custom `localizedDescription` messages don't work as expected. ErrorKit solves this with the `Throwable` protocol, ensuring your error messages always appear as intended. 13 | 14 | ### The Problem with Swift's Error Protocol 15 | 16 | When you create a custom error type in Swift with a `localizedDescription` property: 17 | 18 | ```swift 19 | enum NetworkError: Error { 20 | case noConnectionToServer 21 | case parsingFailed 22 | 23 | var localizedDescription: String { 24 | switch self { 25 | case .noConnectionToServer: "No connection to the server." 26 | case .parsingFailed: "Data parsing failed." 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | You expect to see your custom message when catching the error: 33 | 34 | ```swift 35 | do { 36 | throw NetworkError.noConnectionToServer 37 | } catch { 38 | print(error.localizedDescription) 39 | // Expected: "No connection to the server." 40 | // Actual: "The operation couldn't be completed. (MyApp.NetworkError error 0.)" 41 | } 42 | ``` 43 | 44 | Your custom message never appears! This happens because Swift's `Error` protocol is bridged to `NSError` behind the scenes, which uses a completely different system for error messages with `domain`, `code`, and `userInfo` dictionaries. 45 | 46 | ### The "Official" Solution: LocalizedError 47 | 48 | Swift does provide `LocalizedError` as the officially recommended solution for customizing error messages. However, this protocol has serious issues that make it confusing and error-prone: 49 | 50 | ```swift 51 | enum NetworkError: LocalizedError { 52 | case noConnectionToServer 53 | case parsingFailed 54 | 55 | var errorDescription: String? { // Optional String! 56 | switch self { 57 | case .noConnectionToServer: "No connection to the server." 58 | case .parsingFailed: "Data parsing failed." 59 | } 60 | } 61 | 62 | // Other optional properties that are often ignored 63 | var failureReason: String? { nil } 64 | var recoverySuggestion: String? { nil } 65 | var helpAnchor: String? { nil } 66 | } 67 | ``` 68 | 69 | The problems with `LocalizedError` include: 70 | - All properties are optional (`String?`) – no compiler enforcement 71 | - Only `errorDescription` affects `localizedDescription` – the others are often ignored 72 | - `failureReason` and `recoverySuggestion` are rarely used by Apple frameworks 73 | - `helpAnchor` is an outdated concept rarely used in modern development 74 | - You still need to use String(localized:) for proper localization 75 | 76 | This makes `LocalizedError` both confusing and error-prone, especially for developers new to Swift. 77 | 78 | ### The Solution: Throwable Protocol 79 | 80 | ErrorKit introduces the `Throwable` protocol to solve these problems: 81 | 82 | ```swift 83 | public protocol Throwable: LocalizedError { 84 | var userFriendlyMessage: String { get } 85 | } 86 | ``` 87 | 88 | The `Throwable` protocol is designed to be: 89 | - Named to align with Swift's `throw` keyword for intuitive discovery 90 | - Following Swift's naming convention (`able` suffix like `Codable`) 91 | - Requiring a single, non-optional `userFriendlyMessage` property 92 | - Extending `LocalizedError` for compatibility with existing systems 93 | - Simple and clear with just one requirement 94 | 95 | Here's how you use it: 96 | 97 | ```swift 98 | enum NetworkError: Throwable { 99 | case noConnectionToServer 100 | case parsingFailed 101 | 102 | var userFriendlyMessage: String { 103 | switch self { 104 | case .noConnectionToServer: 105 | String(localized: "Unable to connect to the server.") 106 | case .parsingFailed: 107 | String(localized: "Data parsing failed.") 108 | } 109 | } 110 | } 111 | ``` 112 | 113 | Now when you catch errors: 114 | 115 | ```swift 116 | do { 117 | throw NetworkError.noConnectionToServer 118 | } catch { 119 | print(error.localizedDescription) 120 | // Now correctly shows: "Unable to connect to the server." 121 | } 122 | ``` 123 | 124 | The `Throwable` protocol handles all the mapping between your custom messages and Swift's error system through a default implementation of `LocalizedError.errorDescription` that returns your `userFriendlyMessage`. 125 | 126 | ### Quick Start with Raw Values 127 | 128 | For rapid development and prototyping, `Throwable` automatically works with string raw values: 129 | 130 | ```swift 131 | enum NetworkError: String, Throwable { 132 | case noConnectionToServer = "Unable to connect to the server." 133 | case parsingFailed = "Data parsing failed." 134 | } 135 | ``` 136 | 137 | This eliminates boilerplate by automatically using the raw string values as your error messages. It's perfect for quickly implementing error types during active development before adding proper localization later. 138 | 139 | ### Complete Drop-in Replacement 140 | 141 | `Throwable` is designed as a complete drop-in replacement for `Error`: 142 | 143 | ```swift 144 | // Standard Swift error-handling works exactly the same 145 | func validateUser(name: String) throws { 146 | guard name.count >= 3 else { 147 | throw ValidationError.tooShort 148 | } 149 | } 150 | 151 | // Works with all existing Swift error patterns 152 | do { 153 | try validateUser(name: "Jo") 154 | } catch let error as ValidationError { 155 | // Type-based catching works 156 | handleValidationError(error) 157 | } catch { 158 | // General error catching works 159 | handleGenericError(error) 160 | } 161 | ``` 162 | 163 | Any type that conforms to `Throwable` automatically conforms to `Error`, so you can use it with all existing Swift error handling patterns with no changes to your architecture. 164 | 165 | ## Topics 166 | 167 | ### Essentials 168 | 169 | - ``Throwable`` 170 | 171 | ### Default Implementations 172 | 173 | - ``Swift/RawRepresentable/userFriendlyMessage`` 174 | 175 | ### Error Handling 176 | 177 | - ``ErrorKit/userFriendlyMessage(for:)`` 178 | 179 | ### Continue Reading 180 | 181 | - 182 | -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Guides/Typed-Throws-and-Error-Nesting.md: -------------------------------------------------------------------------------- 1 | # Typed Throws and Error Nesting 2 | 3 | Making Swift 6's typed throws practical with seamless error propagation across layers. 4 | 5 | @Metadata { 6 | @PageImage(purpose: icon, source: "ErrorKit") 7 | @PageImage(purpose: card, source: "TypedThrowsAndNesting") 8 | } 9 | 10 | ## Highlights 11 | 12 | Swift 6 introduces typed throws (`throws(ErrorType)`) for stronger type safety in error handling. ErrorKit makes this powerful feature practical by solving the challenge of error propagation across layers with the `Catching` protocol. 13 | 14 | ### Understanding Typed Throws 15 | 16 | Typed throws let you declare exactly which error types a function can throw: 17 | 18 | ```swift 19 | func processFile() throws(FileError) { 20 | // This function can only throw FileError 21 | } 22 | ``` 23 | 24 | This enables compile-time verification of error handling: 25 | 26 | ```swift 27 | do { 28 | try processFile() 29 | } catch FileError.fileNotFound { 30 | // Handle specific case 31 | } catch FileError.readFailed { 32 | // Handle another specific case 33 | } 34 | // No need for a catch-all since all possibilities are covered 35 | ``` 36 | 37 | ### System Function Overloads 38 | 39 | ErrorKit provides typed-throws overloads for common system APIs. To streamline discovery, these overloads use the same API names prefixed with "throwable": 40 | 41 | ```swift 42 | // Standard system API 43 | try fileManager.createDirectory(at: url) 44 | 45 | // ErrorKit typed overload - same name with "throwable" prefix 46 | try fileManager.throwableCreateDirectory(at: url) 47 | ``` 48 | 49 | The overloaded versions: 50 | - Return the same results as the original functions 51 | - Throw specific error types with detailed information 52 | - Provide better error messages for common failures 53 | 54 | Available overloads include: 55 | 56 | #### FileManager Operations 57 | ```swift 58 | // Creating directories 59 | try FileManager.default.throwableCreateDirectory(at: url) 60 | 61 | // Removing items 62 | try FileManager.default.throwableRemoveItem(at: url) 63 | 64 | // Copying files 65 | try FileManager.default.throwableCopyItem(at: sourceURL, to: destinationURL) 66 | 67 | // Moving files 68 | try FileManager.default.throwableMoveItem(at: sourceURL, to: destinationURL) 69 | ``` 70 | 71 | #### URLSession Operations 72 | ```swift 73 | // Data tasks 74 | let (data, response) = try await URLSession.shared.throwableData(from: url) 75 | 76 | // Handling HTTP status codes 77 | try URLSession.shared.handleHTTPStatusCode(statusCode, data: data) 78 | ``` 79 | 80 | These typed overloads provide a more granular approach to error handling, allowing for precise error handling and improved user experience. 81 | 82 | ### Enhanced User Experience with Typed Throws 83 | 84 | Using typed throws allows developers to implement smarter error handling with specific responses to different error types: 85 | 86 | ```swift 87 | do { 88 | let (data, response) = try await URLSession.shared.throwableData(from: URL(string: "https://api.example.com/data")!) 89 | // Process the data and response 90 | } catch { 91 | // Error is of type `URLSessionError` 92 | switch error { 93 | case .timeout, .requestTimeout, .tooManyRequests: 94 | // Automatically retry the request with a backoff strategy 95 | retryWithExponentialBackoff() 96 | 97 | case .noNetwork: 98 | // Show an SF Symbol indicating the user is offline plus a retry button 99 | showOfflineView() 100 | 101 | case .unauthorized: 102 | // Redirect the user to your login-flow (e.g. because token expired) 103 | startAuthenticationFlow() 104 | 105 | default: 106 | // Fall back to showing error message 107 | showErrorAlert(message: error.localizedDescription) 108 | } 109 | } 110 | ``` 111 | 112 | This specific error handling enables you to: 113 | - Implement automatic retry strategies for transient errors 114 | - Show UI appropriate to the specific error condition 115 | - Trigger authentication flows for permission issues 116 | - Provide a better overall user experience than generic error handling 117 | 118 | ### The Problem: Error Propagation 119 | 120 | While typed throws improves type safety, it creates a challenge when propagating errors through multiple layers of an application. Without ErrorKit, you'd need to manually wrap errors at each layer: 121 | 122 | ```swift 123 | enum ProfileError: Error { 124 | case validationFailed(field: String) 125 | case databaseError(DatabaseError) // Wrapper case needed for database errors 126 | case networkError(NetworkError) // Another wrapper for network errors 127 | 128 | var errorDescription: String { /* ... */ } 129 | } 130 | 131 | func loadProfile(id: String) throws(ProfileError) { 132 | // Regular error throwing for validation 133 | guard id.isValidFormat else { 134 | throw ProfileError.validationFailed(field: "id") 135 | } 136 | 137 | // Manually mapping nested errors 138 | do { 139 | let user = try database.loadUser(id) 140 | do { 141 | let settings = try fileSystem.readUserSettings(user.settingsPath) 142 | return UserProfile(user: user, settings: settings) 143 | } catch let error as NetworkError { 144 | throw ProfileError.networkError(error) // Manual wrapping 145 | } 146 | } catch let error as DatabaseError { 147 | throw ProfileError.databaseError(error) // Manual wrapping 148 | } 149 | } 150 | ``` 151 | 152 | This approach requires: 153 | 1. Creating explicit wrapper cases for each possible error type 154 | 2. Writing repetitive do-catch blocks for manual error conversion 155 | 3. Maintaining this wrapping code as your error types evolve 156 | 157 | ### The Solution: Catching Protocol 158 | 159 | ErrorKit's `Catching` protocol provides a clean solution: 160 | 161 | ```swift 162 | enum ProfileError: Throwable, Catching { 163 | case validationFailed(field: String) 164 | case caught(Error) // Single case handles all nested errors 165 | 166 | var userFriendlyMessage: String { /* ... */ } 167 | } 168 | 169 | func loadProfile(id: String) throws(ProfileError) { 170 | // Regular error throwing for validation 171 | guard id.isValidFormat else { 172 | throw ProfileError.validationFailed(field: "id") 173 | } 174 | 175 | // Automatically wrap any database or file errors 176 | let userData = try ProfileError.catch { 177 | let user = try database.loadUser(id) 178 | let settings = try fileSystem.readUserSettings(user.settingsPath) 179 | return UserProfile(user: user, settings: settings) 180 | } 181 | 182 | return userData 183 | } 184 | ``` 185 | 186 | The `catch` function automatically wraps any errors thrown in its closure into the `caught` case, preserving both type safety and the error's original information. 187 | 188 | ### Best Practices for Using Catching 189 | 190 | To get the most out of the `Catching` protocol, follow these best practices: 191 | 192 | #### 1. When to Add Catching 193 | 194 | Add `Catching` conformance when: 195 | - Your error type might need to wrap errors from lower-level modules 196 | - You're using typed throws and calling functions that throw different error types 197 | - You want to create a hierarchy of errors for better organization 198 | 199 | You'll know you need `Catching` when you see yourself writing error wrapper cases like: 200 | ```swift 201 | enum MyError: Error { 202 | case specificError 203 | case otherModuleError(OtherError) // If you're writing wrapper cases, you need Catching 204 | } 205 | ``` 206 | 207 | #### 2. Error Hierarchy Structure 208 | 209 | Keep your error hierarchies shallow when possible: 210 | - Aim for 2-3 levels at most (e.g., AppError → ModuleError → SystemError) 211 | - Use specific error cases for known errors, and `caught` for others 212 | - Consider organizing by module or feature rather than error type 213 | 214 | #### 3. Preserve User-Friendly Messages 215 | 216 | When implementing `userFriendlyMessage` for a `Catching` type: 217 | 218 | ```swift 219 | var userFriendlyMessage: String { 220 | switch self { 221 | case .specificError: 222 | return "A specific error occurred." 223 | case .caught(let error): 224 | // Use ErrorKit's enhanced messages for wrapped errors 225 | return ErrorKit.userFriendlyMessage(for: error) 226 | } 227 | } 228 | ``` 229 | 230 | This ensures that user-friendly messages propagate correctly through the error chain. 231 | 232 | #### 4. Use with Built-in Error Types 233 | 234 | All of ErrorKit's built-in error types already conform to `Catching`, so you can easily wrap system errors: 235 | 236 | ```swift 237 | func saveUserData() throws(DatabaseError) { 238 | // Automatically wraps SQLite errors, file system errors, etc. 239 | try DatabaseError.catch { 240 | try database.beginTransaction() 241 | try database.execute(query) 242 | try database.commit() 243 | } 244 | } 245 | ``` 246 | 247 | ### Working with Error Chain Debugging 248 | 249 | For complete visibility into your error chains, ErrorKit provides powerful debugging tools that work perfectly with `Catching`. These tools are covered in detail in the [Error Chain Debugging](Error-Chain-Debugging) documentation. 250 | 251 | When using typed throws with the `Catching` protocol, you'll benefit greatly from these debugging capabilities, as they allow you to: 252 | 253 | - Visualize the complete error chain hierarchy 254 | - Track errors through different application layers 255 | - Identify patterns in error propagation 256 | - Prioritize which errors to fix first 257 | 258 | Be sure to explore the error chain debugging features to get the full benefit of ErrorKit's typed throws support. 259 | 260 | ## Topics 261 | 262 | ### Essentials 263 | 264 | - ``Catching`` 265 | - ``Catching/catch(_:)-8kmn1`` 266 | - ``Catching/catch(_:)`` 267 | 268 | ### System Overloads 269 | 270 | - ``FileManager/throwableCreateDirectory(at:withIntermediateDirectories:attributes:)`` 271 | - ``FileManager/throwableRemoveItem(at:)`` 272 | - ``URLSession/throwableData(for:)`` 273 | - ``URLSession/throwableData(from:)`` 274 | - ``URLSession/handleHTTPStatusCode(_:data:)`` 275 | 276 | ### Continue Reading 277 | 278 | - 279 | -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Guides/User-Feedback-with-Logs.md: -------------------------------------------------------------------------------- 1 | # User Feedback with Logs 2 | 3 | Simplify bug reports with automatic log collection from Apple's unified logging system. 4 | 5 | @Metadata { 6 | @PageImage(purpose: icon, source: "ErrorKit") 7 | @PageImage(purpose: card, source: "UserFeedbackWithLogs") 8 | } 9 | 10 | ## Highlights 11 | 12 | When users encounter issues in your app, getting enough context to diagnose the problem is crucial. ErrorKit makes it simple to add diagnostic log collection to your app, providing valuable context for bug reports and support requests. 13 | 14 | ### The Challenge of User Feedback 15 | 16 | When users report problems, they often lack the technical knowledge to provide the necessary details: 17 | - They don't know what information you need to diagnose the issue 18 | - They can't easily access system logs or technical details 19 | - They may struggle to reproduce complex issues on demand 20 | - The steps they describe might be incomplete or unclear 21 | 22 | Without proper context, developers face significant challenges: 23 | - Time wasted in back-and-forth communications asking for more information 24 | - Difficulty reproducing issues that occur only on specific devices or configurations 25 | - Inability to diagnose intermittent problems that happen infrequently 26 | - Frustration for both users and developers as issues remain unresolved 27 | 28 | ### The Power of System Logs 29 | 30 | ErrorKit leverages Apple's unified logging system (`OSLog`/`Logger`) to collect valuable diagnostic information. If you're not already using structured logging, here's a quick introduction: 31 | 32 | ```swift 33 | import OSLog 34 | 35 | // Create a logger - optionally with subsystem and category 36 | let logger = Logger() 37 | // or 38 | let networkLogger = Logger(subsystem: "com.yourapp", category: "networking") 39 | 40 | // Log at appropriate levels 41 | logger.trace("Very detailed tracing info") // Alias for debug 42 | logger.debug("Detailed connection info") // Development debugging 43 | logger.info("User tapped submit button") // General information 44 | logger.notice("Profile successfully loaded") // Important events 45 | logger.warning("Low disk space detected") // Alias for error 46 | logger.error("Failed to load user data") // Errors that should be fixed 47 | logger.critical("Payment processing failed") // Critical issues (alias for fault) 48 | logger.fault("Database corruption detected") // System failures 49 | 50 | // Format values and control privacy 51 | logger.info("User \(userId, privacy: .private) logged in from \(ipAddress, privacy: .public)") 52 | logger.debug("Memory usage: \(bytes, format: .byteCount)") 53 | ``` 54 | 55 | Apple's logging system offers significant advantages over `print()` statements: 56 | - Privacy controls for sensitive data 57 | - Efficient performance with minimal overhead 58 | - Log levels for filtering information 59 | - System-wide integration 60 | - Persistence across app launches 61 | - Console integration for debugging 62 | 63 | ### Comprehensive Log Collection 64 | 65 | A key advantage of ErrorKit's log collection is that it captures not just your app's logs, but also relevant logs from: 66 | 67 | 1. **Third-party frameworks** that use Apple's unified logging system 68 | 2. **System components** your app interacts with (networking, file system, etc.) 69 | 3. **Background processes** related to your app's functionality 70 | 71 | This gives you a complete picture of what was happening in and around your app when the issue occurred, not just the logs you explicitly added. This comprehensive context is often crucial for diagnosing complex issues that involve multiple components. 72 | 73 | ### Creating a Feedback Button 74 | 75 | The easiest way to implement error reporting is with the `.mailComposer` SwiftUI modifier: 76 | 77 | ```swift 78 | struct ContentView: View { 79 | @State private var showMailComposer = false 80 | 81 | var body: some View { 82 | Form { 83 | // Your app content 84 | 85 | Button("Report a Problem") { 86 | showMailComposer = true 87 | } 88 | .mailComposer( 89 | isPresented: $showMailComposer, 90 | recipient: "support@yourapp.com", 91 | subject: "Bug Report", 92 | messageBody: """ 93 | Please describe what happened: 94 | 95 | 96 | 97 | ---------------------------------- 98 | Device: \(UIDevice.current.model) 99 | iOS: \(UIDevice.current.systemVersion) 100 | App version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown") 101 | """, 102 | attachments: [ 103 | try? ErrorKit.logAttachment(ofLast: .minutes(30)) 104 | ] 105 | ) 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | This creates a simple "Report a Problem" button that: 112 | 1. Opens a pre-filled email composer 113 | 2. Includes useful device and app information 114 | 3. Automatically attaches recent system logs 115 | 4. Provides space for the user to describe the issue 116 | 117 | ### Controlling Log Collection 118 | 119 | ErrorKit offers several options for controlling log collection: 120 | 121 | ```swift 122 | // Collect logs from the last 30 minutes with notice level or higher 123 | try ErrorKit.logAttachment(ofLast: .minutes(30), minLevel: .notice) 124 | 125 | // Collect logs from the last hour with error level or higher (fewer, more important logs) 126 | try ErrorKit.logAttachment(ofLast: .hours(1), minLevel: .error) 127 | 128 | // Collect logs from the last 5 minutes with debug level or higher (very detailed) 129 | try ErrorKit.logAttachment(ofLast: .minutes(5), minLevel: .debug) 130 | ``` 131 | 132 | The `minLevel` parameter lets you control how verbose the logs are: 133 | - `.debug`: All logs (very verbose) 134 | - `.info`: Informational logs and above 135 | - `.notice`: Notable events (default) 136 | - `.error`: Only errors and faults 137 | - `.fault`: Only critical errors 138 | 139 | ### Alternative Methods 140 | 141 | If you need more control over log handling, ErrorKit offers additional approaches: 142 | 143 | #### Getting Log Data Directly 144 | 145 | For sending logs to your own backend or processing them in-app: 146 | 147 | ```swift 148 | let logData = try ErrorKit.loggedData( 149 | ofLast: .minutes(10), 150 | minLevel: .notice 151 | ) 152 | 153 | // Use the data with your custom reporting system 154 | analyticsService.sendLogs(data: logData) 155 | ``` 156 | 157 | #### Exporting to a Temporary File 158 | 159 | For sharing logs via other mechanisms: 160 | 161 | ```swift 162 | let logFileURL = try ErrorKit.exportLogFile( 163 | ofLast: .hours(1), 164 | minLevel: .error 165 | ) 166 | 167 | // Share the log file 168 | let activityVC = UIActivityViewController( 169 | activityItems: [logFileURL], 170 | applicationActivities: nil 171 | ) 172 | present(activityVC, animated: true) 173 | ``` 174 | 175 | ### Transforming the Support Experience 176 | 177 | Implementing a feedback button with automatic log collection transforms the support experience for both users and developers: 178 | 179 | #### For Users: 180 | - **Simplified Reporting**: Submit feedback with a single tap, no technical knowledge required 181 | - **No Technical Questions**: Avoid frustrating back-and-forth asking for technical details 182 | - **Faster Resolution**: Issues can be diagnosed and fixed more quickly 183 | - **Better Experience**: Shows users you take their problems seriously with professional tools 184 | 185 | #### For Developers: 186 | - **Complete Context**: See exactly what was happening when the issue occurred 187 | - **Reduced Support Burden**: Less time spent asking for additional information 188 | - **Better Reproduction**: More reliable reproduction steps based on log data 189 | - **Efficient Debugging**: Quickly identify patterns in error reports 190 | - **Developer Sanity**: Stop trying to reproduce issues with insufficient information 191 | 192 | The investment in proper log collection pays dividends in reduced support costs, faster issue resolution, and improved user satisfaction. 193 | 194 | ### Best Practices for Logging 195 | 196 | To maximize the value of ErrorKit's log collection: 197 | 198 | 1. **Use Apple's Logger Instead of Print**: 199 | ```swift 200 | // Instead of: 201 | print("User logged in: \(username)") 202 | 203 | // Use: 204 | Logger().info("User logged in: \(username, privacy: .private)") 205 | ``` 206 | 207 | 2. **Choose Appropriate Log Levels**: 208 | - `.debug` for developer details that are only needed during development 209 | - `.info` for general tracking of normal app flow 210 | - `.notice` for important events users would care about 211 | - `.error` for problems that need fixing but don't prevent core functionality 212 | - `.fault` for critical issues that break core functionality 213 | 214 | 3. **Include Context in Logs**: 215 | ```swift 216 | // Instead of: 217 | Logger().error("Failed to load") 218 | 219 | // Use: 220 | Logger().error("Failed to load document \(documentId): \(error.localizedDescription)") 221 | ``` 222 | 223 | 4. **Protect Sensitive Information**: 224 | ```swift 225 | Logger().info("Processing payment for user \(userId, privacy: .private)") 226 | ``` 227 | 228 | By implementing these best practices along with ErrorKit's log collection, you create a robust system for gathering the context needed to diagnose and fix issues efficiently. 229 | 230 | ## Topics 231 | 232 | ### Essentials 233 | 234 | - ``ErrorKit/logAttachment(ofLast:minLevel:filename:)`` 235 | - ``ErrorKit/loggedData(ofLast:minLevel:)`` 236 | - ``ErrorKit/exportLogFile(ofLast:minLevel:)`` 237 | 238 | ### Helper Types 239 | 240 | - ``MailAttachment`` 241 | - ``Duration/timeInterval`` 242 | - ``Duration/minutes(_:)`` 243 | - ``Duration/hours(_:)`` 244 | -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Resources/BuiltInErrorTypes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/ErrorKit/94d08765d65a7fc7b64730b0a9920ce4246d9fa0/Sources/ErrorKit/ErrorKit.docc/Resources/BuiltInErrorTypes.jpg -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Resources/EnhancedDescriptions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/ErrorKit/94d08765d65a7fc7b64730b0a9920ce4246d9fa0/Sources/ErrorKit/ErrorKit.docc/Resources/EnhancedDescriptions.jpg -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Resources/ErrorChainDebugging.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/ErrorKit/94d08765d65a7fc7b64730b0a9920ce4246d9fa0/Sources/ErrorKit/ErrorKit.docc/Resources/ErrorChainDebugging.jpg -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Resources/ErrorKit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/ErrorKit/94d08765d65a7fc7b64730b0a9920ce4246d9fa0/Sources/ErrorKit/ErrorKit.docc/Resources/ErrorKit.png -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Resources/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/ErrorKit/94d08765d65a7fc7b64730b0a9920ce4246d9fa0/Sources/ErrorKit/ErrorKit.docc/Resources/Logo.png -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Resources/ThrowableProtocol.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/ErrorKit/94d08765d65a7fc7b64730b0a9920ce4246d9fa0/Sources/ErrorKit/ErrorKit.docc/Resources/ThrowableProtocol.jpg -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Resources/TypedThrowsAndNesting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/ErrorKit/94d08765d65a7fc7b64730b0a9920ce4246d9fa0/Sources/ErrorKit/ErrorKit.docc/Resources/TypedThrowsAndNesting.jpg -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/Resources/UserFeedbackWithLogs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/ErrorKit/94d08765d65a7fc7b64730b0a9920ce4246d9fa0/Sources/ErrorKit/ErrorKit.docc/Resources/UserFeedbackWithLogs.jpg -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorKit.docc/theme-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "color": { 4 | "header": "#082B4B", 5 | "documentation-intro-title": "#FFFFFF", 6 | "documentation-intro-figure": "#FFFFFF", 7 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-header) 30%, #000 100%)", 8 | "documentation-intro-accent": "var(--color-header)" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorMapper.swift: -------------------------------------------------------------------------------- 1 | /// A protocol for mapping domain-specific errors to user-friendly messages. 2 | /// 3 | /// `ErrorMapper` allows users to extend ErrorKit's error mapping capabilities by providing custom mappings for errors from specific frameworks, libraries, or domains. 4 | /// 5 | /// # Overview 6 | /// ErrorKit comes with built-in mappers for Foundation, CoreData, and MapKit errors. 7 | /// You can add your own mappers for other frameworks or custom error types using the ``ErrorKit/registerMapper(_:)`` function. 8 | /// ErrorKit will query all registered mappers in reverse order until one returns a non-nil result. This means, the last added mapper takes precedence. 9 | /// 10 | /// # Example Implementation 11 | /// ```swift 12 | /// enum FirebaseErrorMapper: ErrorMapper { 13 | /// static func userFriendlyMessage(for error: Error) -> String? { 14 | /// switch error { 15 | /// case let authError as AuthErrorCode: 16 | /// switch authError.code { 17 | /// case .wrongPassword: 18 | /// return String(localized: "The password is incorrect. Please try again.") 19 | /// case .userNotFound: 20 | /// return String(localized: "No account found with this email address.") 21 | /// default: 22 | /// return nil 23 | /// } 24 | /// 25 | /// case let firestoreError as FirestoreErrorCode.Code: 26 | /// switch firestoreError { 27 | /// case .permissionDenied: 28 | /// return String(localized: "You don't have permission to access this data.") 29 | /// case .unavailable: 30 | /// return String(localized: "The service is temporarily unavailable. Please try again later.") 31 | /// default: 32 | /// return nil 33 | /// } 34 | /// 35 | /// case let storageError as StorageErrorCode: 36 | /// switch storageError { 37 | /// case .objectNotFound: 38 | /// return String(localized: "The requested file could not be found.") 39 | /// case .quotaExceeded: 40 | /// return String(localized: "Storage quota exceeded. Please try again later.") 41 | /// default: 42 | /// return nil 43 | /// } 44 | /// 45 | /// default: 46 | /// return nil 47 | /// } 48 | /// } 49 | /// } 50 | /// 51 | /// // Register during app initialization 52 | /// ErrorKit.registerMapper(FirebaseErrorMapper.self) 53 | /// ``` 54 | /// 55 | /// Your mapper will be called automatically when using ``ErrorKit/userFriendlyMessage(for:)``: 56 | /// ```swift 57 | /// do { 58 | /// let user = try await Auth.auth().signIn(withEmail: email, password: password) 59 | /// } catch { 60 | /// let message = ErrorKit.userFriendlyMessage(for: error) 61 | /// // Message will be generated from FirebaseErrorMapper for Auth/Firestore/Storage errors 62 | /// } 63 | /// ``` 64 | public protocol ErrorMapper { 65 | /// Maps a given error to a user-friendly message if possible. 66 | /// 67 | /// This function is called by ErrorKit when attempting to generate a user-friendly error message. 68 | /// It should check if the error is of a type it can handle and return an appropriate message, or return nil to allow other mappers to process the error. 69 | /// 70 | /// # Implementation Guidelines 71 | /// - Return nil for errors your mapper doesn't handle 72 | /// - Always use String(localized:) for message localization 73 | /// - Keep messages clear, actionable, and non-technical 74 | /// - Avoid revealing sensitive information 75 | /// - Consider the user experience when crafting messages 76 | /// 77 | /// # Example 78 | /// ```swift 79 | /// static func userFriendlyMessage(for error: Error) -> String? { 80 | /// switch error { 81 | /// case let databaseError as DatabaseLibraryError: 82 | /// switch databaseError { 83 | /// case .connectionTimeout: 84 | /// return String(localized: "Database connection timed out. Please try again.") 85 | /// case .queryExecution: 86 | /// return String(localized: "Database query failed. Please contact support.") 87 | /// default: 88 | /// return nil 89 | /// } 90 | /// default: 91 | /// return nil 92 | /// } 93 | /// } 94 | /// ``` 95 | /// 96 | /// - Note: Any error cases you don't provide a return value for will simply keep their original message. So only override the unclear ones or those that are not localized or you want other kinds of improvements for. No need to handle all possible cases just for the sake of it. 97 | /// 98 | /// - Parameter error: The error to potentially map to a user-friendly message 99 | /// - Returns: A user-friendly message if this mapper can handle the error, or nil otherwise 100 | static func userFriendlyMessage(for error: Error) -> String? 101 | } 102 | -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorMappers/CoreDataErrorMapper.swift: -------------------------------------------------------------------------------- 1 | #if canImport(CoreData) 2 | import CoreData 3 | #endif 4 | 5 | enum CoreDataErrorMapper: ErrorMapper { 6 | static func userFriendlyMessage(for error: Error) -> String? { 7 | #if canImport(CoreData) 8 | let nsError = error as NSError 9 | 10 | if nsError.domain == NSCocoaErrorDomain { 11 | switch nsError.code { 12 | 13 | case NSPersistentStoreSaveError: 14 | return String.localized( 15 | key: "EnhancedDescriptions.CoreData.NSPersistentStoreSaveError", 16 | defaultValue: "Failed to save the data. Please try again." 17 | ) 18 | case NSValidationMultipleErrorsError: 19 | return String.localized( 20 | key: "EnhancedDescriptions.CoreData.NSValidationMultipleErrorsError", 21 | defaultValue: "Multiple validation errors occurred while saving." 22 | ) 23 | case NSValidationMissingMandatoryPropertyError: 24 | return String.localized( 25 | key: "EnhancedDescriptions.CoreData.NSValidationMissingMandatoryPropertyError", 26 | defaultValue: "A mandatory property is missing. Please fill all required fields." 27 | ) 28 | case NSValidationRelationshipLacksMinimumCountError: 29 | return String.localized( 30 | key: "EnhancedDescriptions.CoreData.NSValidationRelationshipLacksMinimumCountError", 31 | defaultValue: "A relationship is missing required related objects." 32 | ) 33 | case NSPersistentStoreIncompatibleVersionHashError: 34 | return String.localized( 35 | key: "EnhancedDescriptions.CoreData.NSPersistentStoreIncompatibleVersionHashError", 36 | defaultValue: "The data store is incompatible with the current model version." 37 | ) 38 | case NSPersistentStoreOpenError: 39 | return String.localized( 40 | key: "EnhancedDescriptions.CoreData.NSPersistentStoreOpenError", 41 | defaultValue: "Unable to open the persistent store. Please check your storage or permissions." 42 | ) 43 | case NSManagedObjectValidationError: 44 | return String.localized( 45 | key: "EnhancedDescriptions.CoreData.NSManagedObjectValidationError", 46 | defaultValue: "An object validation error occurred." 47 | ) 48 | default: 49 | return nil 50 | } 51 | } 52 | #endif 53 | 54 | return nil 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorMappers/FoundationErrorMapper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(FoundationNetworking) 3 | import FoundationNetworking 4 | #endif 5 | 6 | enum FoundationErrorMapper: ErrorMapper { 7 | static func userFriendlyMessage(for error: Error) -> String? { 8 | switch error { 9 | 10 | // URLError: Networking errors 11 | case let urlError as URLError: 12 | switch urlError.code { 13 | case .notConnectedToInternet: 14 | return String.localized( 15 | key: "EnhancedDescriptions.URLError.notConnectedToInternet", 16 | defaultValue: "You are not connected to the Internet. Please check your connection." 17 | ) 18 | case .timedOut: 19 | return String.localized( 20 | key: "EnhancedDescriptions.URLError.timedOut", 21 | defaultValue: "The request timed out. Please try again later." 22 | ) 23 | case .cannotFindHost: 24 | return String.localized( 25 | key: "EnhancedDescriptions.URLError.cannotFindHost", 26 | defaultValue: "Unable to find the server. Please check the URL or your network." 27 | ) 28 | case .networkConnectionLost: 29 | return String.localized( 30 | key: "EnhancedDescriptions.URLError.networkConnectionLost", 31 | defaultValue: "The network connection was lost. Please try again." 32 | ) 33 | default: 34 | return String.localized( 35 | key: "EnhancedDescriptions.URLError.default", 36 | defaultValue: "A network error occurred: \(urlError.localizedDescription)" 37 | ) 38 | } 39 | 40 | // CocoaError: File-related errors 41 | case let cocoaError as CocoaError: 42 | switch cocoaError.code { 43 | case .fileNoSuchFile: 44 | return String.localized( 45 | key: "EnhancedDescriptions.CocoaError.fileNoSuchFile", 46 | defaultValue: "The file could not be found." 47 | ) 48 | case .fileReadNoPermission: 49 | return String.localized( 50 | key: "EnhancedDescriptions.CocoaError.fileReadNoPermission", 51 | defaultValue: "You do not have permission to read this file." 52 | ) 53 | case .fileWriteOutOfSpace: 54 | return String.localized( 55 | key: "EnhancedDescriptions.CocoaError.fileWriteOutOfSpace", 56 | defaultValue: "There is not enough disk space to complete the operation." 57 | ) 58 | default: 59 | return String.localized( 60 | key: "EnhancedDescriptions.CocoaError.default", 61 | defaultValue: "A file system error occurred: \(cocoaError.localizedDescription)" 62 | ) 63 | } 64 | 65 | // POSIXError: POSIX errors 66 | case let posixError as POSIXError: 67 | switch posixError.code { 68 | case .ENOSPC: 69 | return String.localized( 70 | key: "EnhancedDescriptions.POSIXError.ENOSPC", 71 | defaultValue: "There is no space left on the device." 72 | ) 73 | case .EACCES: 74 | return String.localized( 75 | key: "EnhancedDescriptions.POSIXError.EACCES", 76 | defaultValue: "Permission denied. Please check your file permissions." 77 | ) 78 | case .EBADF: 79 | return String.localized( 80 | key: "EnhancedDescriptions.POSIXError.EBADF", 81 | defaultValue: "Bad file descriptor. The file may be closed or invalid." 82 | ) 83 | default: 84 | return String.localized( 85 | key: "EnhancedDescriptions.POSIXError.default", 86 | defaultValue: "A system error occurred: \(posixError.localizedDescription)" 87 | ) 88 | } 89 | 90 | default: 91 | return nil 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/ErrorKit/ErrorMappers/MapKitErrorMapper.swift: -------------------------------------------------------------------------------- 1 | #if canImport(MapKit) 2 | import MapKit 3 | #endif 4 | 5 | enum MapKitErrorMapper: ErrorMapper { 6 | static func userFriendlyMessage(for error: Error) -> String? { 7 | #if canImport(MapKit) 8 | if let mkError = error as? MKError { 9 | switch mkError.code { 10 | case .unknown: 11 | return String.localized( 12 | key: "EnhancedDescriptions.MKError.unknown", 13 | defaultValue: "An unknown error occurred in MapKit." 14 | ) 15 | case .serverFailure: 16 | return String.localized( 17 | key: "EnhancedDescriptions.MKError.serverFailure", 18 | defaultValue: "The MapKit server returned an error. Please try again later." 19 | ) 20 | case .loadingThrottled: 21 | return String.localized( 22 | key: "EnhancedDescriptions.MKError.loadingThrottled", 23 | defaultValue: "Map loading is being throttled. Please wait a moment and try again." 24 | ) 25 | case .placemarkNotFound: 26 | return String.localized( 27 | key: "EnhancedDescriptions.MKError.placemarkNotFound", 28 | defaultValue: "The requested placemark could not be found. Please check the location details." 29 | ) 30 | case .directionsNotFound: 31 | return String.localized( 32 | key: "EnhancedDescriptions.MKError.directionsNotFound", 33 | defaultValue: "No directions could be found for the specified route." 34 | ) 35 | default: 36 | return String.localized( 37 | key: "EnhancedDescriptions.MKError.default", 38 | defaultValue: "A MapKit error occurred: \(mkError.localizedDescription)" 39 | ) 40 | } 41 | } 42 | #endif 43 | 44 | return nil 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/ErrorKit/Helpers/Logger+ErrorKit.swift: -------------------------------------------------------------------------------- 1 | #if canImport(OSLog) 2 | import OSLog 3 | 4 | extension Logger { 5 | /// Logs a debug message with complete error chain description 6 | /// 7 | /// ```swift 8 | /// Logger().debug("Operation failed", error: error) 9 | /// ``` 10 | /// 11 | /// - Parameters: 12 | /// - message: The main log message 13 | /// - error: The error to include using its complete chain description for debugging 14 | public func debug(_ message: String, error: some Error) { 15 | let normalizedMessage = self.normalizeMessage(message) 16 | self.debug("\(normalizedMessage):\n\(ErrorKit.errorChainDescription(for: error))") 17 | } 18 | 19 | /// Logs an info message with complete error chain description 20 | /// 21 | /// ```swift 22 | /// Logger().info("User action failed", error: error) 23 | /// ``` 24 | /// 25 | /// - Parameters: 26 | /// - message: The main log message 27 | /// - error: The error to include using its complete chain description for debugging 28 | public func info(_ message: String, error: some Error) { 29 | let normalizedMessage = self.normalizeMessage(message) 30 | self.info("\(normalizedMessage):\n\(ErrorKit.errorChainDescription(for: error))") 31 | } 32 | 33 | /// Logs a notice with complete error chain description 34 | /// 35 | /// ```swift 36 | /// Logger().notice("Important operation failed", error: error) 37 | /// ``` 38 | /// 39 | /// - Parameters: 40 | /// - message: The main log message 41 | /// - error: The error to include using its complete chain description for debugging 42 | public func notice(_ message: String, error: some Error) { 43 | let normalizedMessage = self.normalizeMessage(message) 44 | self.notice("\(normalizedMessage):\n\(ErrorKit.errorChainDescription(for: error))") 45 | } 46 | 47 | /// Logs a warning with complete error chain description 48 | /// 49 | /// ```swift 50 | /// Logger().warning("Recoverable error occurred", error: error) 51 | /// ``` 52 | /// 53 | /// - Parameters: 54 | /// - message: The main log message 55 | /// - error: The error to include using its complete chain description for debugging 56 | public func warning(_ message: String, error: some Error) { 57 | let normalizedMessage = self.normalizeMessage(message) 58 | self.warning("\(normalizedMessage):\n\(ErrorKit.errorChainDescription(for: error))") 59 | } 60 | 61 | /// Logs an error message with complete error chain description 62 | /// 63 | /// ```swift 64 | /// Logger().error("Upload failed", error: error) 65 | /// ``` 66 | /// 67 | /// - Parameters: 68 | /// - message: The main log message 69 | /// - error: The error to include using its complete chain description for debugging 70 | public func error(_ message: String, error: some Error) { 71 | let normalizedMessage = self.normalizeMessage(message) 72 | self.error("\(normalizedMessage):\n\(ErrorKit.errorChainDescription(for: error))") 73 | } 74 | 75 | /// Logs a fault with complete error chain description 76 | /// 77 | /// ```swift 78 | /// Logger().fault("Critical system error", error: error) 79 | /// ``` 80 | /// 81 | /// - Parameters: 82 | /// - message: The main log message 83 | /// - error: The error to include using its complete chain description for debugging 84 | public func fault(_ message: String, error: some Error) { 85 | let normalizedMessage = self.normalizeMessage(message) 86 | self.fault("\(normalizedMessage):\n\(ErrorKit.errorChainDescription(for: error))") 87 | } 88 | 89 | /// Normalizes a log message by removing trailing punctuation and whitespace 90 | /// 91 | /// This ensures consistent formatting regardless of how the user writes the message: 92 | /// - "Upload failed" → "Upload failed" 93 | /// - "Upload failed:" → "Upload failed" 94 | /// - "Upload failed: " → "Upload failed" 95 | /// - "Upload failed." → "Upload failed" 96 | /// 97 | /// - Parameter message: The original message to normalize 98 | /// - Returns: The normalized message without trailing punctuation or whitespace 99 | private func normalizeMessage(_ message: String) -> String { 100 | return message.trimmingCharacters(in: .whitespacesAndNewlines) 101 | .trimmingCharacters(in: CharacterSet(charactersIn: ":")) 102 | } 103 | } 104 | #endif 105 | -------------------------------------------------------------------------------- /Sources/ErrorKit/Helpers/String+ErrorKit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | #if canImport(CryptoKit) 5 | // On Apple platforms, use the modern localization API with Bundle.module 6 | static func localized(key: StaticString, defaultValue: String.LocalizationValue) -> String { 7 | return String( 8 | localized: key, 9 | defaultValue: defaultValue, 10 | bundle: Bundle.module 11 | ) 12 | } 13 | #else 14 | // On non-Apple platforms, just return the default value (the English translation) 15 | static func localized(key: StaticString, defaultValue: String) -> String { 16 | return defaultValue 17 | } 18 | #endif 19 | } 20 | 21 | extension String.StringInterpolation { 22 | /// Interpolates an error using its user-friendly message. 23 | /// 24 | /// Uses ``ErrorKit.userFriendlyMessage(for:)`` to provide clear, actionable error descriptions 25 | /// suitable for displaying to users. For nested errors, returns the message from the root cause. 26 | /// 27 | /// ```swift 28 | /// showAlert("Operation failed: \(error)") 29 | /// ``` 30 | /// 31 | /// - Parameter error: The error to interpolate using its user-friendly message 32 | mutating public func appendInterpolation(_ error: some Error) { 33 | self.appendInterpolation(ErrorKit.userFriendlyMessage(for: error)) 34 | } 35 | 36 | /// Interpolates an error using its complete chain description for debugging. 37 | /// 38 | /// Uses ``ErrorKit.errorChainDescription(for:)`` to show the full error hierarchy, 39 | /// type information, and nested structure. Ideal for logging and debugging. 40 | /// 41 | /// ```swift 42 | /// print("Operation failed with:\n\(chain: error)") 43 | /// // Operation failed with: 44 | /// // DatabaseError 45 | /// // └─ FileError 46 | /// // └─ PermissionError.denied(permission: "~/Downloads/Profile.png") 47 | /// // └─ userFriendlyMessage: "Access to ~/Downloads/Profile.png was declined..." 48 | /// ``` 49 | /// 50 | /// - Parameter error: The error to interpolate using its complete chain description 51 | mutating public func appendInterpolation(chain error: some Error) { 52 | self.appendInterpolation(ErrorKit.errorChainDescription(for: error)) 53 | } 54 | 55 | /// Interpolates an error using its complete chain description for debugging. 56 | /// 57 | /// Uses ``ErrorKit.errorChainDescription(for:)`` to show the full error hierarchy, 58 | /// type information, and nested structure. Ideal for logging and debugging. 59 | /// 60 | /// ```swift 61 | /// print("Operation failed with:\n\(chain: error)") 62 | /// // Operation failed with: 63 | /// // DatabaseError 64 | /// // └─ FileError 65 | /// // └─ PermissionError.denied(permission: "~/Downloads/Profile.png") 66 | /// // └─ userFriendlyMessage: "Access to ~/Downloads/Profile.png was declined..." 67 | /// ``` 68 | /// 69 | /// - Parameter error: The error to interpolate using its complete chain description 70 | /// 71 | /// - NOTE: Alias for ``appendInterpolation(chain:)``. 72 | mutating public func appendInterpolation(debug error: some Error) { 73 | self.appendInterpolation(chain: error) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/ErrorKit/Logging/ErrorKit+OSLog.swift: -------------------------------------------------------------------------------- 1 | #if canImport(OSLog) 2 | import Foundation 3 | import OSLog 4 | 5 | import Foundation 6 | import OSLog 7 | 8 | extension ErrorKit { 9 | /// Returns log data from the unified logging system for a specified time period and minimum level. 10 | /// 11 | /// This function collects logs from your app and generates a string representation that can 12 | /// be attached to support emails or saved for diagnostic purposes. It provides the log data 13 | /// directly rather than creating a file, giving you flexibility in how you use the data. 14 | /// 15 | /// - Parameters: 16 | /// - duration: How far back in time to collect logs. 17 | /// For example, `.minutes(5)` collects logs from the last 5 minutes. 18 | /// - minLevel: The minimum log level to include (default: .notice). 19 | /// Higher levels include less but more important information: 20 | /// - `.debug`: All logs (very verbose) 21 | /// - `.info`: Informational logs and above 22 | /// - `.notice`: Notable events (default) 23 | /// - `.error`: Only errors and faults 24 | /// - `.fault`: Only critical errors 25 | /// 26 | /// - Returns: Data object containing the log content as UTF-8 encoded text 27 | /// - Throws: Errors if log store access fails 28 | /// 29 | /// ## Example: Attach Logs to Support Email 30 | /// ```swift 31 | /// func sendSupportEmail() { 32 | /// do { 33 | /// // Get logs from the last 5 minutes 34 | /// let logData = try ErrorKit.loggedData( 35 | /// ofLast: .seconds(5), 36 | /// minLevel: .notice 37 | /// ) 38 | /// 39 | /// // Create and present mail composer 40 | /// if MFMailComposeViewController.canSendMail() { 41 | /// let mail = MFMailComposeViewController() 42 | /// mail.setToRecipients(["support@yourapp.com"]) 43 | /// mail.setSubject("Support Request") 44 | /// mail.setMessageBody("Please describe your issue here:", isHTML: false) 45 | /// 46 | /// // Attach the log data 47 | /// mail.addAttachmentData( 48 | /// logData, 49 | /// mimeType: "text/plain", 50 | /// fileName: "app_logs.txt" 51 | /// ) 52 | /// 53 | /// present(mail, animated: true) 54 | /// } 55 | /// } catch { 56 | /// // Handle log export error 57 | /// showAlert(message: "Could not attach logs: \(error.localizedDescription)") 58 | /// } 59 | /// } 60 | /// ``` 61 | /// 62 | /// - See Also: ``exportLogFile(ofLast:minLevel:)`` for when you need a URL with the log content written to a text file 63 | public static func loggedData(ofLast duration: Duration, minLevel: OSLogEntryLog.Level = .notice) throws -> Data { 64 | let logStore = try OSLogStore(scope: .currentProcessIdentifier) 65 | 66 | let fromDate = Date.now.advanced(by: -duration.timeInterval) 67 | let fromDatePosition = logStore.position(date: fromDate) 68 | 69 | let levelPredicate = NSPredicate(format: "level >= %d", minLevel.rawValue) 70 | 71 | let entries = try logStore.getEntries(with: [.reverse], at: fromDatePosition, matching: levelPredicate) 72 | let logMessages = entries.map(\.composedMessage).joined(separator: "\n") 73 | return Data(logMessages.utf8) 74 | } 75 | 76 | /// Exports logs from the unified logging system to a file for a specified time period and minimum level. 77 | /// 78 | /// This convenience function builds on ``loggedData(ofLast:minLevel:)`` by writing the log data 79 | /// to a temporary file. This is useful when working with APIs that require a file URL rather than Data. 80 | /// 81 | /// - Parameters: 82 | /// - duration: How far back in time to collect logs 83 | /// - minLevel: The minimum log level to include (default: .notice) 84 | /// 85 | /// - Returns: URL to the temporary file containing the exported logs 86 | /// - Throws: Errors if log store access fails or if writing to the file fails 87 | /// 88 | /// - See Also: ``loggedData(ofLast:minLevel:)`` for when you need the log content as Data directly 89 | public static func exportLogFile(ofLast duration: Duration, minLevel: OSLogEntryLog.Level = .notice) throws -> URL { 90 | let logData = try loggedData(ofLast: duration, minLevel: minLevel) 91 | 92 | let fileName = "logs_\(Date.now.formatted(.iso8601)).txt" 93 | let fileURL = FileManager.default.temporaryDirectory.appending(path: fileName) 94 | 95 | try logData.write(to: fileURL) 96 | return fileURL 97 | } 98 | 99 | /// Creates a mail attachment containing log data from the unified logging system. 100 | /// 101 | /// This convenience function builds on the logging functionality to create a ready-to-use 102 | /// mail attachment for including logs in support emails or bug reports. 103 | /// 104 | /// - Parameters: 105 | /// - duration: How far back in time to collect logs. 106 | /// For example, `.minutes(5)` collects logs from the last 5 minutes. 107 | /// - minLevel: The minimum log level to include (default: .notice). 108 | /// Higher levels include less but more important information: 109 | /// - `.debug`: All logs (very verbose) 110 | /// - `.info`: Informational logs and above 111 | /// - `.notice`: Notable events (default) 112 | /// - `.error`: Only errors and faults 113 | /// - `.fault`: Only critical errors 114 | /// - filename: Optional custom filename for the log attachment (default: "app_logs_[timestamp].txt") 115 | /// 116 | /// - Returns: A `MailAttachment` ready to be used with the mail composer 117 | /// - Throws: Errors if log store access fails 118 | /// 119 | /// ## Example: Attach Logs to Support Email 120 | /// ```swift 121 | /// Button("Report Problem") { 122 | /// do { 123 | /// // Get logs from the last hour as a mail attachment 124 | /// let logAttachment = try ErrorKit.logAttachment( 125 | /// ofLast: .minutes(60), 126 | /// minLevel: .notice 127 | /// ) 128 | /// 129 | /// showMailComposer = true 130 | /// } catch { 131 | /// errorMessage = "Could not prepare logs: \(error.localizedDescription)" 132 | /// showError = true 133 | /// } 134 | /// } 135 | /// .mailComposer( 136 | /// isPresented: $showMailComposer, 137 | /// recipients: ["support@yourapp.com"], 138 | /// subject: "Bug Report", 139 | /// messageBody: "I encountered the following issue:", 140 | /// attachments: [logAttachment] 141 | /// ) 142 | /// ``` 143 | public static func logAttachment( 144 | ofLast duration: Duration, 145 | minLevel: OSLogEntryLog.Level = .notice, 146 | filename: String? = nil 147 | ) throws -> MailAttachment { 148 | let logData = try loggedData(ofLast: duration, minLevel: minLevel) 149 | 150 | let attachmentFilename = filename ?? "app_logs_\(Date.now.formatted(.iso8601)).txt" 151 | 152 | return MailAttachment( 153 | data: logData, 154 | mimeType: "text/plain", 155 | filename: attachmentFilename 156 | ) 157 | } 158 | } 159 | 160 | extension Duration { 161 | /// Returns the duration as a `TimeInterval`. 162 | /// 163 | /// This can be useful for interfacing with APIs that require `TimeInterval` (which is measured in seconds), allowing you to convert a `Duration` directly to the needed format. 164 | /// 165 | /// Example: 166 | /// ```swift 167 | /// let duration = Duration.hours(2) 168 | /// let timeInterval = duration.timeInterval // Converts to TimeInterval for compatibility 169 | /// ``` 170 | /// 171 | /// - Returns: The duration as a `TimeInterval`, which represents the duration in seconds. 172 | public var timeInterval: TimeInterval { 173 | TimeInterval(self.components.seconds) + (TimeInterval(self.components.attoseconds) / 1_000_000_000_000_000_000) 174 | } 175 | 176 | /// Constructs a `Duration` given a number of minutes represented as a `BinaryInteger`. 177 | /// 178 | /// This is helpful for precise time measurements, such as cooking timers, short breaks, or meeting durations. 179 | /// 180 | /// Example: 181 | /// ```swift 182 | /// let fifteenMinutesDuration = Duration.minutes(15) // Creates a Duration of 15 minutes 183 | /// ``` 184 | /// 185 | /// - Parameter minutes: The number of minutes. 186 | /// - Returns: A `Duration` representing the given number of minutes. 187 | public static func minutes(_ minutes: T) -> Duration { 188 | self.seconds(minutes * 60) 189 | } 190 | 191 | /// Constructs a `Duration` given a number of hours represented as a `BinaryInteger`. 192 | /// 193 | /// Can be used to schedule events or tasks that are several hours long. 194 | /// 195 | /// Example: 196 | /// ```swift 197 | /// let eightHoursDuration = Duration.hours(8) // Creates a Duration of 8 hours 198 | /// ``` 199 | /// 200 | /// - Parameter hours: The number of hours. 201 | /// - Returns: A `Duration` representing the given number of hours. 202 | public static func hours(_ hours: T) -> Duration { 203 | self.minutes(hours * 60) 204 | } 205 | } 206 | #endif 207 | -------------------------------------------------------------------------------- /Sources/ErrorKit/Logging/MailAttachment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents an email attachment with data, mime type, and filename 4 | public struct MailAttachment { 5 | let data: Data 6 | let mimeType: String 7 | let filename: String 8 | 9 | /// Creates a new email attachment 10 | /// - Parameters: 11 | /// - data: The content of the attachment as Data 12 | /// - mimeType: The MIME type of the attachment (e.g., "image/jpeg", "application/pdf") 13 | /// - filename: The filename for the attachment when received by the recipient 14 | public init(data: Data, mimeType: String, filename: String) { 15 | self.data = data 16 | self.mimeType = mimeType 17 | self.filename = filename 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ErrorKit/Logging/MailComposerModifier.swift: -------------------------------------------------------------------------------- 1 | #if canImport(MessageUI) 2 | import SwiftUI 3 | import MessageUI 4 | 5 | /// A view modifier that presents a mail composer for sending emails with attachments. 6 | /// This modifier is particularly useful for implementing feedback or bug reporting features. 7 | struct MailComposerModifier: ViewModifier { 8 | @Environment(\.dismiss) private var dismiss 9 | 10 | @Binding var isPresented: Bool 11 | 12 | var recipient: String 13 | var subject: String? 14 | var messageBody: String? 15 | var attachments: [MailAttachment?] 16 | 17 | func body(content: Content) -> some View { 18 | content 19 | .sheet(isPresented: self.$isPresented) { 20 | if MailComposerView.canSendMail() { 21 | MailComposerView( 22 | isPresented: self.$isPresented, 23 | recipients: [self.recipient], 24 | subject: self.subject, 25 | messageBody: self.messageBody, 26 | attachments: self.attachments 27 | ) 28 | } else { 29 | VStack(spacing: 20) { 30 | Text( 31 | String( 32 | localized: "Logging.MailComposer.notAvailableTitle", 33 | defaultValue: "Mail Not Available", 34 | bundle: .module 35 | ) 36 | ) 37 | .font(.headline) 38 | 39 | Text( 40 | String( 41 | localized: "Logging.MailComposer.notAvailableMessage", 42 | defaultValue: "Your device is not configured to send emails. Please set up the Mail app or use another method to contact support at: \(self.recipient)", 43 | bundle: .module, 44 | comment: "%@ is typically replaced by the email address of the support contact, e.g. 'support@example.com' – so this would read like '... contact support at: support@example.com'" 45 | ) 46 | ) 47 | .multilineTextAlignment(.center) 48 | .padding(.horizontal) 49 | 50 | Button( 51 | String( 52 | localized: "Logging.MailComposer.dismissButton", 53 | defaultValue: "Dismiss", 54 | bundle: .module 55 | ) 56 | ) { 57 | self.dismiss() 58 | } 59 | .buttonStyle(.borderedProminent) 60 | } 61 | .padding() 62 | } 63 | } 64 | } 65 | } 66 | 67 | /// Extension that adds the mailComposer modifier to any SwiftUI view. 68 | extension View { 69 | /// Presents a mail composer when a binding to a Boolean value becomes `true`. 70 | /// 71 | /// Use this modifier to present an email composition interface with optional 72 | /// recipients, subject, message body, and attachments (such as log files). 73 | /// 74 | /// # Example 75 | /// ```swift 76 | /// struct ContentView: View { 77 | /// @State private var showMailComposer = false 78 | /// 79 | /// var body: some View { 80 | /// Button("Report Problem") { 81 | /// showMailComposer = true 82 | /// } 83 | /// .mailComposer( 84 | /// isPresented: $showMailComposer, 85 | /// recipient: "support@yourapp.com", 86 | /// subject: "App Feedback", 87 | /// messageBody: "I encountered an issue while using the app:", 88 | /// attachments: [try? ErrorKit.logAttachment(ofLast: .minutes(10))] 89 | /// ) 90 | /// } 91 | /// } 92 | /// ``` 93 | /// 94 | /// - Parameters: 95 | /// - isPresented: A binding to a Boolean value that determines whether to present the mail composer. 96 | /// - recipient: The email address to include in the "To" field. 97 | /// - subject: The subject line of the email. 98 | /// - messageBody: The content of the email message. 99 | /// - attachments: An array of attachments to include with the email. 100 | /// - Returns: A view that presents a mail composer when `isPresented` is `true`. 101 | public func mailComposer( 102 | isPresented: Binding, 103 | recipient: String, 104 | subject: String? = nil, 105 | messageBody: String? = nil, 106 | attachments: [MailAttachment?] = [] 107 | ) -> some View { 108 | self.modifier( 109 | MailComposerModifier( 110 | isPresented: isPresented, 111 | recipient: recipient, 112 | subject: subject, 113 | messageBody: messageBody, 114 | attachments: attachments 115 | ) 116 | ) 117 | } 118 | } 119 | #endif 120 | -------------------------------------------------------------------------------- /Sources/ErrorKit/Logging/MailComposerView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(MessageUI) 2 | import SwiftUI 3 | import MessageUI 4 | 5 | /// A SwiftUI component that wraps UIKit's MFMailComposeViewController to provide email composition functionality in SwiftUI applications. 6 | struct MailComposerView: UIViewControllerRepresentable { 7 | /// Checks if the device is capable of sending emails 8 | /// - Returns: Boolean indicating whether email composition is available 9 | static func canSendMail() -> Bool { 10 | MFMailComposeViewController.canSendMail() 11 | } 12 | 13 | @Binding var isPresented: Bool 14 | 15 | var recipients: [String]? 16 | var subject: String? 17 | var messageBody: String? 18 | var attachments: [MailAttachment?] 19 | 20 | func makeUIViewController(context: Context) -> MFMailComposeViewController { 21 | let composer = MFMailComposeViewController() 22 | composer.mailComposeDelegate = context.coordinator 23 | 24 | if let recipients { 25 | composer.setToRecipients(recipients) 26 | } 27 | 28 | if let subject { 29 | composer.setSubject(subject) 30 | } 31 | 32 | if let messageBody { 33 | composer.setMessageBody(messageBody, isHTML: false) 34 | } 35 | 36 | for attachment in attachments { 37 | if let attachment { 38 | composer.addAttachmentData( 39 | attachment.data, 40 | mimeType: attachment.mimeType, 41 | fileName: attachment.filename 42 | ) 43 | } 44 | } 45 | 46 | return composer 47 | } 48 | 49 | func updateUIViewController(_ uiViewController: MFMailComposeViewController, context: Context) {} 50 | 51 | func makeCoordinator() -> Coordinator { 52 | Coordinator(self) 53 | } 54 | 55 | class Coordinator: NSObject, @preconcurrency MFMailComposeViewControllerDelegate { 56 | var parent: MailComposerView 57 | 58 | init(_ parent: MailComposerView) { 59 | self.parent = parent 60 | } 61 | 62 | @MainActor 63 | func mailComposeController( 64 | _ controller: MFMailComposeViewController, 65 | didFinishWith result: MFMailComposeResult, 66 | error: Error? 67 | ) { 68 | self.parent.isPresented = false 69 | controller.dismiss(animated: true) 70 | } 71 | } 72 | } 73 | #endif 74 | -------------------------------------------------------------------------------- /Sources/ErrorKit/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Sources/ErrorKit/Throwable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A protocol that makes error handling in Swift more intuitive by requiring a `userFriendlyMessage` property. 4 | /// 5 | /// `Throwable` extends `LocalizedError` and simplifies the process of defining error messages, 6 | /// ensuring that developers can provide meaningful feedback for errors without the confusion associated with Swift's native `Error` and `LocalizedError` types. 7 | /// 8 | /// ### Key Features: 9 | /// - Requires a `userFriendlyMessage`, making it easier to provide custom error messages. 10 | /// - Offers a default implementation for `errorDescription`, ensuring smooth integration with `LocalizedError` and `.localizedDescription`. 11 | /// - Supports `RawRepresentable` enums with `String` as `RawValue` to minimize boilerplate. 12 | /// 13 | /// ### Why Use `Throwable`? 14 | /// - **Simplified API**: Unlike `LocalizedError`, `Throwable` focuses on a single requirement: `userFriendlyMessage`. 15 | /// - **Intuitive Naming**: The name aligns with Swift's `throw` keyword and other common `-able` protocols like `Codable`. 16 | /// - **Readable Error Handling**: Provides concise, human-readable error descriptions. 17 | /// 18 | /// ### Usage Example: 19 | /// 20 | /// #### 1. Custom Error with Manual `userFriendlyMessage`: 21 | /// ```swift 22 | /// enum NetworkError: Throwable { 23 | /// case noConnectionToServer 24 | /// case parsingFailed 25 | /// 26 | /// var userFriendlyMessage: String { 27 | /// switch self { 28 | /// case .noConnectionToServer: "Unable to connect to the server." 29 | /// case .parsingFailed: "Data parsing failed." 30 | /// } 31 | /// } 32 | /// } 33 | /// ``` 34 | /// 35 | /// #### 2. Custom Error Using `RawRepresentable` for Minimal Boilerplate: 36 | /// ```swift 37 | /// enum NetworkError: String, Throwable { 38 | /// case noConnectionToServer = "Unable to connect to the server." 39 | /// case parsingFailed = "Data parsing failed." 40 | /// } 41 | /// ``` 42 | /// 43 | /// #### 3. Throwing and Catching Errors: 44 | /// ```swift 45 | /// struct ContentView: View { 46 | /// var body: some View { 47 | /// Button("Throw Random NetworkError") { 48 | /// do { 49 | /// throw NetworkError.allCases.randomElement()! 50 | /// } catch { 51 | /// print("Caught error with message: \(error.localizedDescription)") 52 | /// } 53 | /// } 54 | /// } 55 | /// } 56 | /// ``` 57 | /// Output: 58 | /// ``` 59 | /// Caught error with message: Unable to connect to the server. 60 | /// ``` 61 | /// 62 | public protocol Throwable: LocalizedError, Sendable { 63 | /// A human-readable error message describing the error. 64 | var userFriendlyMessage: String { get } 65 | } 66 | 67 | // MARK: - Default Implementations 68 | 69 | /// Provides a default implementation for `Throwable` when the conforming type is a `RawRepresentable` with a `String` raw value. 70 | /// 71 | /// This allows enums with `String` raw values to automatically use the raw value as the error's `userFriendlyMessage`. 72 | extension Throwable where Self: RawRepresentable, RawValue == String { 73 | public var userFriendlyMessage: String { 74 | self.rawValue 75 | } 76 | } 77 | 78 | /// Provides a default implementation for `errorDescription` required by `LocalizedError`, ensuring it returns the value of `userFriendlyMessage`. 79 | extension Throwable { 80 | public var errorDescription: String? { 81 | self.userFriendlyMessage 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/ErrorKit/TypedOverloads/FileManager+ErrorKit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An enumeration that represents various errors that can occur when performing file management operations. 4 | public enum FileManagerError: Throwable { 5 | /// The specified file or directory could not be found. 6 | /// - This error occurs when an operation targets a file or directory that doesn't exist. 7 | case fileNotFound 8 | 9 | /// You do not have permission to read the specified file or directory. 10 | /// - This error happens when the system denies read access due to permission restrictions. 11 | case noReadPermission 12 | 13 | /// You do not have permission to write to the specified file or directory. 14 | /// - This error occurs when the system denies write access due to permission restrictions. 15 | case noWritePermission 16 | 17 | /// There is not enough disk space to complete the operation. 18 | /// - This error occurs when the file system runs out of space while attempting to write or copy files. 19 | case outOfSpace 20 | 21 | /// The file name is invalid and cannot be used. 22 | /// - This error happens when the specified file name contains illegal characters or formats. 23 | case invalidFileName 24 | 25 | /// The file is corrupted or in an unreadable format. 26 | /// - This error occurs when attempting to read a file that is damaged or in an unsupported format. 27 | case corruptFile 28 | 29 | /// The file is locked and cannot be modified. 30 | /// - This error happens when attempting to modify or delete a file that is locked by another process or the system. 31 | case fileLocked 32 | 33 | /// An unknown error occurred while reading the file. 34 | /// - This error is thrown when an unexpected issue happens during a read operation that doesn't match other error types. 35 | case readError 36 | 37 | /// An unknown error occurred while writing the file. 38 | /// - This error is thrown when an unexpected issue happens during a write operation that doesn't match other error types. 39 | case writeError 40 | 41 | /// The file's character encoding is not supported. 42 | /// - This error occurs when the system cannot decode the file due to an unsupported character encoding. 43 | case unsupportedEncoding 44 | 45 | /// The file is too large to be processed. 46 | /// - This error occurs when a file exceeds system limits, such as memory constraints, making it impossible to handle. 47 | case fileTooLarge 48 | 49 | /// The storage volume is read-only and cannot be modified. 50 | /// - This error happens when attempting to modify a file on a read-only volume, such as a disk or network drive. 51 | case volumeReadOnly 52 | 53 | /// The file or directory already exists. 54 | /// - This error is thrown when attempting to create a file or directory that already exists at the specified location. 55 | case fileExists 56 | 57 | /// A general error case for any other unforeseen errors. 58 | /// - This error is used when the underlying error does not match any of the predefined cases and is passed as a wrapped error. 59 | case other(Error) 60 | 61 | /// Returns a user-friendly error message based on the error case. 62 | /// 63 | /// The message is localized for the user, with a default fallback message. 64 | public var userFriendlyMessage: String { 65 | switch self { 66 | case .fileNotFound: 67 | String.localized( 68 | key: "TypedOverloads.FileManager.fileNotFound", 69 | defaultValue: "The specified file or directory could not be found." 70 | ) 71 | case .noReadPermission: 72 | String.localized( 73 | key: "TypedOverloads.FileManager.noReadPermission", 74 | defaultValue: "You do not have permission to read this file or directory." 75 | ) 76 | case .noWritePermission: 77 | String.localized( 78 | key: "TypedOverloads.FileManager.noWritePermission", 79 | defaultValue: "You do not have permission to write to this file or directory." 80 | ) 81 | case .outOfSpace: 82 | String.localized( 83 | key: "TypedOverloads.FileManager.outOfSpace", 84 | defaultValue: "There is not enough disk space to complete the operation." 85 | ) 86 | case .invalidFileName: 87 | String.localized( 88 | key: "TypedOverloads.FileManager.invalidFileName", 89 | defaultValue: "The file name is invalid and cannot be used." 90 | ) 91 | case .corruptFile: 92 | String.localized( 93 | key: "TypedOverloads.FileManager.corruptFile", 94 | defaultValue: "The file is corrupted or in an unreadable format." 95 | ) 96 | case .fileLocked: 97 | String.localized( 98 | key: "TypedOverloads.FileManager.fileLocked", 99 | defaultValue: "The file is locked and cannot be modified." 100 | ) 101 | case .readError: 102 | String.localized( 103 | key: "TypedOverloads.FileManager.readError", 104 | defaultValue: "An unknown error occurred while reading the file." 105 | ) 106 | case .writeError: 107 | String.localized( 108 | key: "TypedOverloads.FileManager.writeError", 109 | defaultValue: "An unknown error occurred while writing the file." 110 | ) 111 | case .unsupportedEncoding: 112 | String.localized( 113 | key: "TypedOverloads.FileManager.unsupportedEncoding", 114 | defaultValue: "The file's character encoding is not supported." 115 | ) 116 | case .fileTooLarge: 117 | String.localized( 118 | key: "TypedOverloads.FileManager.fileTooLarge", 119 | defaultValue: "The file is too large to be processed." 120 | ) 121 | case .volumeReadOnly: 122 | String.localized( 123 | key: "TypedOverloads.FileManager.volumeReadOnly", 124 | defaultValue: "The storage volume is read-only and cannot be modified." 125 | ) 126 | case .fileExists: 127 | String.localized( 128 | key: "TypedOverloads.FileManager.fileExists", 129 | defaultValue: "The file or directory already exists." 130 | ) 131 | case .other(let error): 132 | ErrorKit.userFriendlyMessage(for: error) 133 | } 134 | } 135 | } 136 | 137 | extension FileManager { 138 | /// A typed-throws overload of `createDirectory(at:)` that maps known errors to a custom ``FileManagerError`` enum for enhanced error handling. 139 | public func throwableCreateDirectory(at url: URL, withIntermediateDirectories createIntermediates: Bool = false, attributes: [FileAttributeKey : Any]? = nil) throws(FileManagerError) { 140 | do { 141 | try self.createDirectory(at: url, withIntermediateDirectories: createIntermediates, attributes: attributes) 142 | } catch { 143 | throw self.mapToThrowable(error: error as NSError) 144 | } 145 | } 146 | 147 | /// A typed-throws overload of `createDirectory(atPath:)` that maps known errors to a custom ``FileManagerError`` enum for enhanced error handling. 148 | public func throwableCreateDirectory(atPath path: String, withIntermediateDirectories createIntermediates: Bool = false, attributes: [FileAttributeKey : Any]? = nil) throws(FileManagerError) { 149 | do { 150 | try self.createDirectory(atPath: path, withIntermediateDirectories: createIntermediates, attributes: attributes) 151 | } catch { 152 | throw self.mapToThrowable(error: error as NSError) 153 | } 154 | } 155 | 156 | /// A typed-throws overload of `removeItem(at:)` that maps known errors to a custom ``FileManagerError`` enum for enhanced error handling. 157 | public func throwableRemoveItem(at url: URL) throws(FileManagerError) { 158 | do { 159 | try self.removeItem(at: url) 160 | } catch { 161 | throw self.mapToThrowable(error: error as NSError) 162 | } 163 | } 164 | 165 | /// A typed-throws overload of `removeItem(atPath:)` that maps known errors to a custom ``FileManagerError`` enum for enhanced error handling. 166 | public func throwableRemoveItem(atPath path: String) throws(FileManagerError) { 167 | do { 168 | try self.removeItem(atPath: path) 169 | } catch { 170 | throw self.mapToThrowable(error: error as NSError) 171 | } 172 | } 173 | 174 | /// A typed-throws overload of `copyItem(at:)` that maps known errors to a custom ``FileManagerError`` enum for enhanced error handling. 175 | public func throwableCopyItem(at sourceURL: URL, to destinationURL: URL) throws(FileManagerError) { 176 | do { 177 | try self.copyItem(at: sourceURL, to: destinationURL) 178 | } catch { 179 | throw self.mapToThrowable(error: error as NSError) 180 | } 181 | } 182 | 183 | /// A typed-throws overload of `copyItem(atPath:)` that maps known errors to a custom ``FileManagerError`` enum for enhanced error handling. 184 | public func throwableCopyItem(atPath sourcePath: String, toPath destinationPath: String) throws(FileManagerError) { 185 | do { 186 | try self.copyItem(atPath: sourcePath, toPath: destinationPath) 187 | } catch { 188 | throw self.mapToThrowable(error: error as NSError) 189 | } 190 | } 191 | 192 | /// A typed-throws overload of `moveItem(at:)` that maps known errors to a custom ``FileManagerError`` enum for enhanced error handling. 193 | public func throwableMoveItem(at sourceURL: URL, to destinationURL: URL) throws(FileManagerError) { 194 | do { 195 | try self.moveItem(at: sourceURL, to: destinationURL) 196 | } catch { 197 | throw self.mapToThrowable(error: error as NSError) 198 | } 199 | } 200 | 201 | /// A typed-throws overload of `moveItem(atPath:)` that maps known errors to a custom ``FileManagerError`` enum for enhanced error handling. 202 | public func throwableMoveItem(atPath sourcePath: String, toPath destinationPath: String) throws(FileManagerError) { 203 | do { 204 | try self.moveItem(atPath: sourcePath, toPath: destinationPath) 205 | } catch { 206 | throw self.mapToThrowable(error: error as NSError) 207 | } 208 | } 209 | 210 | /// A typed-throws overload of `attributesOfItem(atPath:)` that maps known errors to a custom ``FileManagerError`` enum for enhanced error handling. 211 | public func throwableAttributesOfItem(atPath path: String) throws(FileManagerError) -> [FileAttributeKey: Any] { 212 | do { 213 | return try self.attributesOfItem(atPath: path) 214 | } catch { 215 | throw self.mapToThrowable(error: error as NSError) 216 | } 217 | } 218 | 219 | private func mapToThrowable(error: NSError) -> FileManagerError { 220 | switch (error.domain, error.code) { 221 | case (NSCocoaErrorDomain, NSFileNoSuchFileError): .fileNotFound 222 | case (NSCocoaErrorDomain, NSFileReadNoPermissionError): .noReadPermission 223 | case (NSCocoaErrorDomain, NSFileWriteNoPermissionError): .noWritePermission 224 | case (NSCocoaErrorDomain, NSFileWriteOutOfSpaceError): .outOfSpace 225 | case (NSCocoaErrorDomain, NSFileWriteInvalidFileNameError): .invalidFileName 226 | case (NSCocoaErrorDomain, NSFileReadCorruptFileError): .corruptFile 227 | case (NSCocoaErrorDomain, NSFileLockingError): .fileLocked 228 | case (NSCocoaErrorDomain, NSFileReadUnknownError): .readError 229 | case (NSCocoaErrorDomain, NSFileWriteUnknownError): .writeError 230 | case (NSCocoaErrorDomain, NSFileReadInapplicableStringEncodingError): .unsupportedEncoding 231 | case (NSCocoaErrorDomain, NSFileReadTooLargeError): .fileTooLarge 232 | case (NSCocoaErrorDomain, NSFileWriteVolumeReadOnlyError): .volumeReadOnly 233 | case (NSCocoaErrorDomain, NSFileWriteFileExistsError): .fileExists 234 | default: .other(error) 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Sources/ErrorKit/TypedOverloads/URLSession+ErrorKit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(FoundationNetworking) 3 | import FoundationNetworking 4 | #endif 5 | 6 | /// An enumeration that represents various errors that can occur when performing network requests with `URLSession`. 7 | public enum URLSessionError: Throwable { 8 | /// The request timed out. 9 | case timeout 10 | 11 | /// The're no network connection. 12 | case noNetwork 13 | 14 | /// The host could not be found. 15 | case cannotFindHost 16 | 17 | /// Something was wrong with the URL. 18 | case badURL 19 | 20 | /// The network request was cancelled. 21 | case cancelled 22 | 23 | /// An SSL error occurred during the request. 24 | case sslError 25 | 26 | /// A network error occurred that doesn't match a specific case. 27 | case networkError(Error) 28 | 29 | /// The server returned a 401 Unauthorized status code. 30 | case unauthorized(bodyData: Data?) 31 | 32 | /// The server returned a 402 Payment Required status code. 33 | case paymentRequired(bodyData: Data?) 34 | 35 | /// The server returned a 403 Forbidden status code. 36 | case forbidden(bodyData: Data?) 37 | 38 | /// The server returned a 404 Not Found status code. 39 | case notFound(bodyData: Data?) 40 | 41 | /// The server returned a 405 Method Not Allowed status code. 42 | case methodNotAllowed(bodyData: Data?) 43 | 44 | /// The server returned a 406 Not Acceptable status code. 45 | case notAcceptable(bodyData: Data?) 46 | 47 | /// The server returned a 408 Request Timeout status code. 48 | case requestTimeout(bodyData: Data?) 49 | 50 | /// The server returned a 409 Conflict status code. 51 | case conflict(bodyData: Data?) 52 | 53 | /// The server returned a 415 Unsupported Media Type status code. 54 | case unsupportedMediaType(bodyData: Data?) 55 | 56 | /// The server returned a 429 Too Many Requests status code. 57 | case tooManyRequests(bodyData: Data?) 58 | 59 | /// The server returned a generic 4xx Bad Request status code (fallback). 60 | case badRequest(bodyData: Data?) 61 | 62 | /// The server returned a 500 Internal Server Error status code. 63 | case serverError 64 | 65 | /// An unknown error occurred. 66 | case unknownStatusCode(Int) 67 | 68 | /// A general error case for any other unforeseen errors. 69 | case other(Error) 70 | 71 | public var userFriendlyMessage: String { 72 | switch self { 73 | case .timeout: 74 | return String.localized( 75 | key: "TypedOverloads.URLSession.timeout", 76 | defaultValue: "The request timed out. Please try again." 77 | ) 78 | case .noNetwork: 79 | return String.localized( 80 | key: "TypedOverloads.URLSession.noNetwork", 81 | defaultValue: "No network connection found. Please check your internet." 82 | ) 83 | case .cannotFindHost: 84 | return String.localized( 85 | key: "TypedOverloads.URLSession.cannotFindHost", 86 | defaultValue: "Cannot find host. Please check your internet connection and try again." 87 | ) 88 | case .badURL: 89 | return String.localized( 90 | key: "TypedOverloads.URLSession.badURL", 91 | defaultValue: "The URL is malformed. Please check it and try again or report a bug." 92 | ) 93 | case .cancelled: 94 | return String.localized( 95 | key: "TypedOverloads.URLSession.cancelled", 96 | defaultValue: "The request was cancelled. Please try again if this wasn't intended." 97 | ) 98 | case .sslError: 99 | return String.localized( 100 | key: "TypedOverloads.URLSession.sslError", 101 | defaultValue: "There was an SSL error. Please check the server's certificate." 102 | ) 103 | case .networkError(let error): 104 | return ErrorKit.userFriendlyMessage(for: error) 105 | case .unauthorized: 106 | return String.localized( 107 | key: "TypedOverloads.URLSession.unauthorized", 108 | defaultValue: "You are not authorized to access this resource (401)." 109 | ) 110 | case .paymentRequired: 111 | return String.localized( 112 | key: "TypedOverloads.URLSession.paymentRequired", 113 | defaultValue: "Payment is required to access this resource (402)." 114 | ) 115 | case .forbidden: 116 | return String.localized( 117 | key: "TypedOverloads.URLSession.forbidden", 118 | defaultValue: "You do not have permission to access this resource (403)." 119 | ) 120 | case .notFound: 121 | return String.localized( 122 | key: "TypedOverloads.URLSession.notFound", 123 | defaultValue: "The requested resource could not be found (404)." 124 | ) 125 | case .methodNotAllowed: 126 | return String.localized( 127 | key: "TypedOverloads.URLSession.methodNotAllowed", 128 | defaultValue: "The HTTP method is not allowed for this resource (405)." 129 | ) 130 | case .notAcceptable: 131 | return String.localized( 132 | key: "TypedOverloads.URLSession.notAcceptable", 133 | defaultValue: "The requested resource cannot produce an acceptable response (406)." 134 | ) 135 | case .requestTimeout: 136 | return String.localized( 137 | key: "TypedOverloads.URLSession.requestTimeout", 138 | defaultValue: "The request timed out (408). Please try again." 139 | ) 140 | case .conflict: 141 | return String.localized( 142 | key: "TypedOverloads.URLSession.conflict", 143 | defaultValue: "There was a conflict with the request (409). Please review and try again." 144 | ) 145 | case .unsupportedMediaType: 146 | return String.localized( 147 | key: "TypedOverloads.URLSession.unsupportedMediaType", 148 | defaultValue: "The request entity has an unsupported media type (415)." 149 | ) 150 | case .tooManyRequests: 151 | return String.localized( 152 | key: "TypedOverloads.URLSession.tooManyRequests", 153 | defaultValue: "Too many requests have been sent. Please wait and try again (429)." 154 | ) 155 | case .badRequest: 156 | return String.localized( 157 | key: "TypedOverloads.URLSession.badRequest", 158 | defaultValue: "The request was malformed (400). Please review and try again." 159 | ) 160 | case .serverError: 161 | return String.localized( 162 | key: "TypedOverloads.URLSession.serverError", 163 | defaultValue: "The server encountered an error (500). Please try again later." 164 | ) 165 | case .unknownStatusCode(let statusCode): 166 | return String.localized( 167 | key: "TypedOverloads.URLSession.unknown", 168 | defaultValue: "An unknown status code was received from the server: \(statusCode)" 169 | ) 170 | case .other(let error): 171 | return ErrorKit.userFriendlyMessage(for: error) 172 | } 173 | } 174 | } 175 | 176 | extension URLSession { 177 | /// A typed-throws overload of `data(for:)` that maps known errors to a custom `URLSessionError` enum for enhanced error handling. 178 | public func throwableData(for request: URLRequest) async throws -> (Data, URLResponse) { 179 | do { 180 | return try await self.data(for: request) 181 | } catch { 182 | throw mapToThrowable(error: error as NSError) 183 | } 184 | } 185 | 186 | /// A typed-throws overload of `data(from:)` that maps known errors to a custom `URLSessionError` enum for enhanced error handling. 187 | public func throwableData(from url: URL) async throws -> (Data, URLResponse) { 188 | do { 189 | return try await self.data(from: url) 190 | } catch { 191 | throw mapToThrowable(error: error as NSError) 192 | } 193 | } 194 | 195 | private func mapToThrowable(error: NSError) -> URLSessionError { 196 | switch (error.domain, error.code) { 197 | case (NSURLErrorDomain, NSURLErrorTimedOut): .timeout 198 | case (NSURLErrorDomain, NSURLErrorNetworkConnectionLost): .noNetwork 199 | case (NSURLErrorDomain, NSURLErrorNotConnectedToInternet): .noNetwork 200 | case (NSURLErrorDomain, NSURLErrorCannotFindHost): .cannotFindHost 201 | case (NSURLErrorDomain, NSURLErrorCannotConnectToHost): .serverError 202 | case (NSURLErrorDomain, NSURLErrorCancelled): .cancelled 203 | case (NSURLErrorDomain, NSURLErrorBadURL): .badURL 204 | case (NSURLErrorDomain, NSURLErrorSecureConnectionFailed): .sslError 205 | default: .networkError(error) 206 | } 207 | } 208 | 209 | /// A method to handle HTTP status codes and provide better error handling for different cases. 210 | public func handleHTTPStatusCode(_ statusCode: Int, data: Data?) throws -> Data? { 211 | switch statusCode { 212 | case 200...299: // Success range 213 | return data 214 | case 400...499: // Client errors 215 | switch statusCode { 216 | case 401: 217 | throw URLSessionError.unauthorized(bodyData: data) 218 | case 402: 219 | throw URLSessionError.paymentRequired(bodyData: data) 220 | case 403: 221 | throw URLSessionError.forbidden(bodyData: data) 222 | case 404: 223 | throw URLSessionError.notFound(bodyData: data) 224 | case 405: 225 | throw URLSessionError.methodNotAllowed(bodyData: data) 226 | case 406: 227 | throw URLSessionError.notAcceptable(bodyData: data) 228 | case 408: 229 | throw URLSessionError.requestTimeout(bodyData: data) 230 | case 409: 231 | throw URLSessionError.conflict(bodyData: data) 232 | case 415: 233 | throw URLSessionError.unsupportedMediaType(bodyData: data) 234 | case 429: 235 | throw URLSessionError.tooManyRequests(bodyData: data) 236 | default: 237 | throw URLSessionError.badRequest(bodyData: data) 238 | } 239 | case 500...599: // Server errors 240 | throw URLSessionError.serverError 241 | default: 242 | throw URLSessionError.unknownStatusCode(statusCode) 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /Tests/ErrorKitTests/ErrorKitTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import ErrorKit 4 | 5 | enum ErrorKitTests { 6 | struct SomeLocalizedError: LocalizedError { 7 | let errorDescription: String? = "Something failed." 8 | let failureReason: String? = "It failed because it wanted to." 9 | let recoverySuggestion: String? = "Try again later." 10 | let helpAnchor: String? = "https://github.com/apple/swift-error-kit#readme" 11 | } 12 | 13 | struct SomeThrowable: Throwable { 14 | let userFriendlyMessage: String = "Something failed hard." 15 | } 16 | 17 | enum UserFriendlyMessage { 18 | @Test 19 | static func localizedError() { 20 | #expect(ErrorKit.userFriendlyMessage(for: SomeLocalizedError()) == "Something failed. It failed because it wanted to. Try again later.") 21 | } 22 | 23 | #if canImport(CryptoKit) 24 | @Test 25 | static func nsError() { 26 | let nsError = NSError(domain: "SOME", code: 1245, userInfo: [NSLocalizedDescriptionKey: "Something failed."]) 27 | #expect(ErrorKit.userFriendlyMessage(for: nsError) == "[SOME: 1245] Something failed.") 28 | } 29 | #endif 30 | 31 | @Test 32 | static func throwable() async throws { 33 | #expect(ErrorKit.userFriendlyMessage(for: SomeThrowable()) == "Something failed hard.") 34 | } 35 | 36 | @Test 37 | static func nested() async throws { 38 | let nestedError = DatabaseError.caught(FileError.caught(PermissionError.denied(permission: "~/Downloads/Profile.png"))) 39 | #expect(ErrorKit.userFriendlyMessage(for: nestedError) == "Access to ~/Downloads/Profile.png was declined. To use this feature, please enable the permission in your device Settings.") 40 | } 41 | } 42 | 43 | enum StringInterpolation { 44 | @Test 45 | static func implicitWithStruct() async throws { 46 | #expect("\(SomeThrowable())" == "Something failed hard.") 47 | } 48 | 49 | @Test 50 | static func implicitWithNestedError() async throws { 51 | let nestedError = DatabaseError.caught(FileError.caught(PermissionError.denied(permission: "~/Downloads/Profile.png"))) 52 | #expect("\(nestedError)" == "Access to ~/Downloads/Profile.png was declined. To use this feature, please enable the permission in your device Settings.") 53 | } 54 | 55 | @Test 56 | static func chainWithStruct() async throws { 57 | #expect( 58 | "\(chain: SomeThrowable())" 59 | == 60 | """ 61 | SomeThrowable [Struct] 62 | └─ userFriendlyMessage: "Something failed hard." 63 | """ 64 | ) 65 | } 66 | 67 | @Test 68 | static func chainWithNestedError() async throws { 69 | let nestedError = DatabaseError.caught(FileError.caught(PermissionError.denied(permission: "~/Downloads/Profile.png"))) 70 | #expect( 71 | "\(chain: nestedError)" 72 | == 73 | """ 74 | DatabaseError 75 | └─ FileError 76 | └─ PermissionError.denied(permission: "~/Downloads/Profile.png") 77 | └─ userFriendlyMessage: "Access to ~/Downloads/Profile.png was declined. To use this feature, please enable the permission in your device Settings." 78 | """) 79 | } 80 | } 81 | 82 | enum ErrorChainDescription { 83 | @Test 84 | static func localizedError() { 85 | #expect( 86 | ErrorKit.errorChainDescription(for: SomeLocalizedError()) 87 | == 88 | """ 89 | SomeLocalizedError [Struct] 90 | └─ userFriendlyMessage: "Something failed. It failed because it wanted to. Try again later." 91 | """ 92 | ) 93 | } 94 | 95 | #if canImport(CryptoKit) 96 | @Test 97 | static func nsError() { 98 | let nsError = NSError(domain: "SOME", code: 1245, userInfo: [NSLocalizedDescriptionKey: "Something failed."]) 99 | let generatedErrorChainDescription = ErrorKit.errorChainDescription(for: nsError) 100 | let expectedErrorChainDescription = """ 101 | NSError [Class] 102 | └─ userFriendlyMessage: "[SOME: 1245] Something failed." 103 | """ 104 | #expect(generatedErrorChainDescription == expectedErrorChainDescription) 105 | } 106 | #endif 107 | 108 | @Test 109 | static func throwableStruct() { 110 | let generatedErrorChainDescription = ErrorKit.errorChainDescription(for: SomeThrowable()) 111 | let expectedErrorChainDescription = """ 112 | SomeThrowable [Struct] 113 | └─ userFriendlyMessage: "Something failed hard." 114 | """ 115 | #expect(generatedErrorChainDescription == expectedErrorChainDescription) 116 | } 117 | 118 | @Test 119 | static func throwableEnum() { 120 | let generatedErrorChainDescription = ErrorKit.errorChainDescription(for: PermissionError.restricted(permission: "~/Downloads/Profile.png")) 121 | let expectedErrorChainDescription = """ 122 | PermissionError.restricted(permission: "~/Downloads/Profile.png") 123 | └─ userFriendlyMessage: "Access to ~/Downloads/Profile.png is currently restricted. This may be due to system settings or parental controls." 124 | """ 125 | #expect(generatedErrorChainDescription == expectedErrorChainDescription) 126 | } 127 | 128 | @Test 129 | static func shallowNested() { 130 | let nestedError = DatabaseError.caught(FileError.fileNotFound(fileName: "App.sqlite")) 131 | let generatedErrorChainDescription = ErrorKit.errorChainDescription(for: nestedError) 132 | let expectedErrorChainDescription = """ 133 | DatabaseError 134 | └─ FileError.fileNotFound(fileName: "App.sqlite") 135 | └─ userFriendlyMessage: "The file App.sqlite could not be located. Please verify the file path and try again." 136 | """ 137 | #expect(generatedErrorChainDescription == expectedErrorChainDescription) 138 | } 139 | 140 | @Test 141 | static func deeplyNestedThrowablesWithEnumLeaf() { 142 | let nestedError = StateError.caught( 143 | OperationError.caught( 144 | DatabaseError.caught( 145 | FileError.caught( 146 | PermissionError.denied(permission: "~/Downloads/Profile.png") 147 | ) 148 | ) 149 | ) 150 | ) 151 | let generatedErrorChainDescription = ErrorKit.errorChainDescription(for: nestedError) 152 | let expectedErrorChainDescription = """ 153 | StateError 154 | └─ OperationError 155 | └─ DatabaseError 156 | └─ FileError 157 | └─ PermissionError.denied(permission: "~/Downloads/Profile.png") 158 | └─ userFriendlyMessage: "Access to ~/Downloads/Profile.png was declined. To use this feature, please enable the permission in your device Settings." 159 | """ 160 | #expect(generatedErrorChainDescription == expectedErrorChainDescription) 161 | } 162 | 163 | @Test 164 | static func deeplyNestedThrowablesWithStructLeaf() { 165 | let nestedError = StateError.caught( 166 | OperationError.caught( 167 | DatabaseError.caught( 168 | FileError.caught( 169 | SomeThrowable() 170 | ) 171 | ) 172 | ) 173 | ) 174 | let generatedErrorChainDescription = ErrorKit.errorChainDescription(for: nestedError) 175 | let expectedErrorChainDescription = """ 176 | StateError 177 | └─ OperationError 178 | └─ DatabaseError 179 | └─ FileError 180 | └─ SomeThrowable [Struct] 181 | └─ userFriendlyMessage: "Something failed hard." 182 | """ 183 | #expect(generatedErrorChainDescription == expectedErrorChainDescription) 184 | } 185 | 186 | @Test 187 | static func deeplyNestedThrowablesWithLocalizedErrorLeaf() { 188 | let nestedError = StateError.caught( 189 | OperationError.caught( 190 | DatabaseError.caught( 191 | FileError.caught( 192 | SomeLocalizedError() 193 | ) 194 | ) 195 | ) 196 | ) 197 | let generatedErrorChainDescription = ErrorKit.errorChainDescription(for: nestedError) 198 | let expectedErrorChainDescription = """ 199 | StateError 200 | └─ OperationError 201 | └─ DatabaseError 202 | └─ FileError 203 | └─ SomeLocalizedError [Struct] 204 | └─ userFriendlyMessage: "Something failed. It failed because it wanted to. Try again later." 205 | """ 206 | #expect(generatedErrorChainDescription == expectedErrorChainDescription) 207 | } 208 | 209 | #if canImport(CryptoKit) 210 | @Test 211 | static func deeplyNestedThrowablesWithNSErrorLeaf() { 212 | let nsError = NSError(domain: "SOME", code: 1245, userInfo: [NSLocalizedDescriptionKey: "Something failed."]) 213 | let nestedError = StateError.caught( 214 | OperationError.caught( 215 | DatabaseError.caught( 216 | FileError.caught(nsError) 217 | ) 218 | ) 219 | ) 220 | let generatedErrorChainDescription = ErrorKit.errorChainDescription(for: nestedError) 221 | let expectedErrorChainDescription = """ 222 | StateError 223 | └─ OperationError 224 | └─ DatabaseError 225 | └─ FileError 226 | └─ NSError [Class] 227 | └─ userFriendlyMessage: "[SOME: 1245] Something failed." 228 | """ 229 | #expect(generatedErrorChainDescription == expectedErrorChainDescription) 230 | } 231 | #endif 232 | } 233 | 234 | // TODO: add more tests for more specific errors such as CoreData, MapKit – and also nested errors! 235 | } 236 | -------------------------------------------------------------------------------- /Tests/ErrorKitTests/ThrowableTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import ErrorKit 3 | 4 | enum ExplicitDescriptionError: Throwable { 5 | case somethingFailed 6 | case reuestTimeout 7 | 8 | var userFriendlyMessage: String { 9 | switch self { 10 | case .somethingFailed: "Something failed unexpectedly" 11 | case .reuestTimeout: "Request timed out" 12 | } 13 | } 14 | } 15 | 16 | @Test 17 | func explicitDescriptionOutput() async throws { 18 | do { 19 | throw ExplicitDescriptionError.somethingFailed 20 | } catch { 21 | #expect(error.localizedDescription == "Something failed unexpectedly") 22 | } 23 | } 24 | 25 | enum RawValueDescriptionError: String, Throwable { 26 | case somethingFailed = "Something failed unexpectedly" 27 | case reqestTimeout = "Request timed out" 28 | } 29 | 30 | @Test 31 | func rawValueDescriptionOutput() async throws { 32 | do { 33 | throw RawValueDescriptionError.reqestTimeout 34 | } catch { 35 | #expect(error.localizedDescription == "Request timed out") 36 | } 37 | } 38 | --------------------------------------------------------------------------------