Skip to content

Commit 2296c3b

Browse files
committed
Require Sendable conformance for Throwable + add new README section
1 parent badc95c commit 2296c3b

File tree

3 files changed

+360
-1
lines changed

3 files changed

+360
-1
lines changed

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,87 @@ Here, the code leverages the specific error types to implement various kinds of
209209
### Summary
210210

211211
By utilizing these typed-throws overloads, you can write more robust and maintainable code. ErrorKit's enhanced user-friendly messages and ability to handle specific errors with code lead to a better developer and user experience. As the library continues to evolve, we encourage the community to contribute additional overloads and error types for common system APIs to further enhance its capabilities.
212+
213+
214+
## Built-in Error Types for Common Scenarios
215+
216+
ErrorKit provides a set of pre-defined error types for common scenarios that developers encounter frequently. These built-in types conform to `Throwable` and can be used with both typed throws (`throws(DatabaseError)`) and classical throws declarations.
217+
218+
### Why Built-in Types?
219+
220+
Built-in error types offer several advantages:
221+
- **Quick Start**: Begin with well-structured error handling without defining custom types
222+
- **Consistency**: Use standardized error cases and messages across your codebase
223+
- **Flexibility**: Easily transition to custom error types when you need more specific cases
224+
- **Discoverability**: Clear naming conventions make it easy to find the right error type
225+
- **Localization**: All error messages are pre-localized and user-friendly
226+
- **Ecosystem Impact**: As more Swift packages adopt these standardized error types, apps can implement smarter error handling that works across dependencies. Instead of just showing error messages, apps could provide specific UI or recovery actions for known error types, creating a more cohesive error handling experience throughout the ecosystem.
227+
228+
### Available Error Types
229+
230+
ErrorKit includes the following built-in error types:
231+
232+
- **DatabaseError** (connectionFailed, operationFailed, recordNotFound)
233+
- **FileError** (fileNotFound, readFailed, writeFailed)
234+
- **NetworkError** (noInternet, timeout, badRequest, serverError, decodingFailure)
235+
- **OperationError** (dependencyFailed, canceled, unknownFailure)
236+
- **ParsingError** (invalidInput, missingField, inputTooLong)
237+
- **PermissionError** (denied, restricted, notDetermined)
238+
- **StateError** (invalidState, alreadyFinalized, preconditionFailed)
239+
- **ValidationError** (invalidInput, missingField, inputTooLong)
240+
- **GenericError** (for ad-hoc custom messages)
241+
242+
All built-in error types include a `generic` case that accepts a custom `userFriendlyMessage`, allowing for quick additions of edge cases without creating new error types. Use the `GenericError` struct when you want to quickly throw a one-off error without having to define your own type if none of the other fit, useful especially during early phases of development.
243+
244+
### Usage Examples
245+
246+
```swift
247+
func fetchUserData() throws(DatabaseError) {
248+
guard isConnected else {
249+
throw .connectionFailed
250+
}
251+
// Fetching logic
252+
}
253+
254+
// Or with classical throws
255+
func processData() throws {
256+
guard isValid else {
257+
throw ValidationError.invalidInput(field: "email")
258+
}
259+
// Processing logic
260+
}
261+
262+
// Quick error throwing with GenericError
263+
func quickOperation() throws {
264+
guard condition else {
265+
throw GenericError(userFriendlyMessage: String(localized: "The condition X was not fulfilled, please check again."))
266+
}
267+
// Operation logic
268+
}
269+
270+
// Using generic case for edge cases
271+
func handleSpecialCase() throws(DatabaseError) {
272+
guard specialCondition else {
273+
throw .generic(userFriendlyMessage: String(localized: "Database is in maintenance mode"))
274+
}
275+
// Special case handling
276+
}
277+
```
278+
279+
### Contributing New Error Types
280+
281+
We need your help! If you find yourself:
282+
- Defining similar error types across projects
283+
- Missing a common error scenario in our built-in types
284+
- Seeing patterns in error handling that could benefit others
285+
- Having ideas for better error messages or new cases
286+
287+
Please contribute! Submit a pull request to add your error types or cases to ErrorKit. Your contribution helps build a more robust error handling ecosystem for Swift developers.
288+
289+
When contributing:
290+
- Ensure error cases are generic enough for broad use
291+
- Provide clear, actionable error messages
292+
- Include real-world usage examples in documentation
293+
- Follow the existing naming conventions
294+
295+
Together, we can build a comprehensive set of error types that cover most common scenarios in Swift development and create a more unified error handling experience across the ecosystem.

Sources/ErrorKit/Resources/Localizable.xcstrings

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,281 @@
11
{
22
"sourceLanguage" : "en",
33
"strings" : {
4+
"BuiltInErrors.DatabaseError.connectionFailed" : {
5+
"extractionState" : "extracted_with_value",
6+
"localizations" : {
7+
"en" : {
8+
"stringUnit" : {
9+
"state" : "new",
10+
"value" : "Failed to connect to the database. Please try again later."
11+
}
12+
}
13+
}
14+
},
15+
"BuiltInErrors.DatabaseError.operationFailed" : {
16+
"extractionState" : "extracted_with_value",
17+
"localizations" : {
18+
"en" : {
19+
"stringUnit" : {
20+
"state" : "new",
21+
"value" : "An error occurred while performing the operation: %@. Please try again."
22+
}
23+
}
24+
}
25+
},
26+
"BuiltInErrors.DatabaseError.recordNotFound" : {
27+
"extractionState" : "extracted_with_value",
28+
"localizations" : {
29+
"en" : {
30+
"stringUnit" : {
31+
"state" : "new",
32+
"value" : "The %1$@ record could not be found.%2$@ Please check and try again."
33+
}
34+
}
35+
}
36+
},
37+
"BuiltInErrors.FileError.fileNotFound" : {
38+
"extractionState" : "extracted_with_value",
39+
"localizations" : {
40+
"en" : {
41+
"stringUnit" : {
42+
"state" : "new",
43+
"value" : "The file %@ could not be found. Please check the file path."
44+
}
45+
}
46+
}
47+
},
48+
"BuiltInErrors.FileError.readError" : {
49+
"extractionState" : "extracted_with_value",
50+
"localizations" : {
51+
"en" : {
52+
"stringUnit" : {
53+
"state" : "new",
54+
"value" : "There was an issue reading the file %@. Please try again."
55+
}
56+
}
57+
}
58+
},
59+
"BuiltInErrors.FileError.writeError" : {
60+
"extractionState" : "extracted_with_value",
61+
"localizations" : {
62+
"en" : {
63+
"stringUnit" : {
64+
"state" : "new",
65+
"value" : "There was an issue writing to the file %@. Please try again."
66+
}
67+
}
68+
}
69+
},
70+
"BuiltInErrors.NetworkError.badRequest" : {
71+
"extractionState" : "extracted_with_value",
72+
"localizations" : {
73+
"en" : {
74+
"stringUnit" : {
75+
"state" : "new",
76+
"value" : "The request was malformed (%1$lld): %2$@. Please review and try again."
77+
}
78+
}
79+
}
80+
},
81+
"BuiltInErrors.NetworkError.decodingFailure" : {
82+
"extractionState" : "extracted_with_value",
83+
"localizations" : {
84+
"en" : {
85+
"stringUnit" : {
86+
"state" : "new",
87+
"value" : "The data received from the server could not be processed. Please try again."
88+
}
89+
}
90+
}
91+
},
92+
"BuiltInErrors.NetworkError.noInternet" : {
93+
"extractionState" : "extracted_with_value",
94+
"localizations" : {
95+
"en" : {
96+
"stringUnit" : {
97+
"state" : "new",
98+
"value" : "No internet connection is available. Please check your network settings and try again."
99+
}
100+
}
101+
}
102+
},
103+
"BuiltInErrors.NetworkError.serverError" : {
104+
"extractionState" : "extracted_with_value",
105+
"localizations" : {
106+
"en" : {
107+
"stringUnit" : {
108+
"state" : "new",
109+
"value" : "The server encountered an error (Code: %lld)."
110+
}
111+
}
112+
}
113+
},
114+
"BuiltInErrors.NetworkError.timeout" : {
115+
"extractionState" : "extracted_with_value",
116+
"localizations" : {
117+
"en" : {
118+
"stringUnit" : {
119+
"state" : "new",
120+
"value" : "The request timed out. Please try again later."
121+
}
122+
}
123+
}
124+
},
125+
"BuiltInErrors.OperationError.canceled" : {
126+
"extractionState" : "extracted_with_value",
127+
"localizations" : {
128+
"en" : {
129+
"stringUnit" : {
130+
"state" : "new",
131+
"value" : "The operation was canceled. Please try again if necessary."
132+
}
133+
}
134+
}
135+
},
136+
"BuiltInErrors.OperationError.dependencyFailed" : {
137+
"extractionState" : "extracted_with_value",
138+
"localizations" : {
139+
"en" : {
140+
"stringUnit" : {
141+
"state" : "new",
142+
"value" : "The operation could not be completed due to a failed dependency: %@."
143+
}
144+
}
145+
}
146+
},
147+
"BuiltInErrors.OperationError.unknownFailure" : {
148+
"extractionState" : "extracted_with_value",
149+
"localizations" : {
150+
"en" : {
151+
"stringUnit" : {
152+
"state" : "new",
153+
"value" : "The operation failed due to an unknown reason: %@. Please try again or contact support."
154+
}
155+
}
156+
}
157+
},
158+
"BuiltInErrors.ParsingError.invalidInput" : {
159+
"extractionState" : "extracted_with_value",
160+
"localizations" : {
161+
"en" : {
162+
"stringUnit" : {
163+
"state" : "new",
164+
"value" : "The provided input is invalid: %@. Please correct it and try again."
165+
}
166+
}
167+
}
168+
},
169+
"BuiltInErrors.ParsingError.missingField" : {
170+
"extractionState" : "extracted_with_value",
171+
"localizations" : {
172+
"en" : {
173+
"stringUnit" : {
174+
"state" : "new",
175+
"value" : "A required field is missing: %@. Please review and try again."
176+
}
177+
}
178+
}
179+
},
180+
"BuiltInErrors.PermissionError.denied" : {
181+
"extractionState" : "extracted_with_value",
182+
"localizations" : {
183+
"en" : {
184+
"stringUnit" : {
185+
"state" : "new",
186+
"value" : "The %@ permission was denied. Please enable it in Settings to continue."
187+
}
188+
}
189+
}
190+
},
191+
"BuiltInErrors.PermissionError.notDetermined" : {
192+
"extractionState" : "extracted_with_value",
193+
"localizations" : {
194+
"en" : {
195+
"stringUnit" : {
196+
"state" : "new",
197+
"value" : "The %@ permission has not been determined. Please try again or check your Settings."
198+
}
199+
}
200+
}
201+
},
202+
"BuiltInErrors.PermissionError.restricted" : {
203+
"extractionState" : "extracted_with_value",
204+
"localizations" : {
205+
"en" : {
206+
"stringUnit" : {
207+
"state" : "new",
208+
"value" : "The %@ permission is restricted. This may be due to parental controls or other system restrictions."
209+
}
210+
}
211+
}
212+
},
213+
"BuiltInErrors.StateError.alreadyFinalized" : {
214+
"extractionState" : "extracted_with_value",
215+
"localizations" : {
216+
"en" : {
217+
"stringUnit" : {
218+
"state" : "new",
219+
"value" : "The operation cannot be performed because the state is already finalized."
220+
}
221+
}
222+
}
223+
},
224+
"BuiltInErrors.StateError.invalidState" : {
225+
"extractionState" : "extracted_with_value",
226+
"localizations" : {
227+
"en" : {
228+
"stringUnit" : {
229+
"state" : "new",
230+
"value" : "The operation cannot proceed due to an invalid state: %@."
231+
}
232+
}
233+
}
234+
},
235+
"BuiltInErrors.StateError.preconditionFailed" : {
236+
"extractionState" : "extracted_with_value",
237+
"localizations" : {
238+
"en" : {
239+
"stringUnit" : {
240+
"state" : "new",
241+
"value" : "A required condition was not met: %@. Please review and try again."
242+
}
243+
}
244+
}
245+
},
246+
"BuiltInErrors.ValidationError.inputTooLong" : {
247+
"extractionState" : "extracted_with_value",
248+
"localizations" : {
249+
"en" : {
250+
"stringUnit" : {
251+
"state" : "new",
252+
"value" : "%1$@ exceeds the maximum allowed length of %2$lld characters. Please shorten it."
253+
}
254+
}
255+
}
256+
},
257+
"BuiltInErrors.ValidationError.invalidInput" : {
258+
"extractionState" : "extracted_with_value",
259+
"localizations" : {
260+
"en" : {
261+
"stringUnit" : {
262+
"state" : "new",
263+
"value" : "The value provided for %@ is invalid. Please correct it."
264+
}
265+
}
266+
}
267+
},
268+
"BuiltInErrors.ValidationError.missingField" : {
269+
"extractionState" : "extracted_with_value",
270+
"localizations" : {
271+
"en" : {
272+
"stringUnit" : {
273+
"state" : "new",
274+
"value" : "%@ is a required field. Please provide a value."
275+
}
276+
}
277+
}
278+
},
4279
"EnhancedDescriptions.CocoaError.default" : {
5280
"extractionState" : "extracted_with_value",
6281
"localizations" : {

Sources/ErrorKit/Throwable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import Foundation
5959
/// Caught error with message: Unable to connect to the server.
6060
/// ```
6161
///
62-
public protocol Throwable: LocalizedError {
62+
public protocol Throwable: LocalizedError, Sendable {
6363
/// A human-readable error message describing the error.
6464
var userFriendlyMessage: String { get }
6565
}

0 commit comments

Comments
 (0)