├── .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 | 
2 |
3 | [](https://swiftpackageindex.com/FlineDev/ErrorKit)
4 | [](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 | App Icon |
225 | App Name & Description |
226 | Supported Platforms |
227 |
228 |
229 |
230 |
231 |
232 |
233 | |
234 |
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 | |
241 | Mac |
242 |
243 |
244 |
245 |
246 |
247 |
248 | |
249 |
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 | |
256 | iPhone, iPad, Mac, Vision |
257 |
258 |
259 |
260 |
261 |
262 |
263 | |
264 |
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 | |
271 | Mac |
272 |
273 |
274 |
275 |
276 |
277 |
278 | |
279 |
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 | |
286 | iPhone, iPad, Mac, Vision |
287 |
288 |
289 |
290 |
291 |
292 |
293 | |
294 |
295 |
296 | CrossCraft: Custom Crosswords
297 |
298 |
299 | Create themed & personalized crosswords. Solve them yourself or share them to challenge others.
300 | |
301 | iPhone, iPad, Mac, Vision |
302 |
303 |
304 |
305 |
306 |
307 |
308 | |
309 |
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 | |
316 | iPhone, iPad, Mac, Vision |
317 |
318 |
319 |
320 |
321 |
322 |
323 | |
324 |
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 | |
331 | Vision |
332 |
333 |
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 |
--------------------------------------------------------------------------------