Skip to content

Commit db8a7d4

Browse files
authored
Merge pull request #14 from FlineDev/wip/common-errors
Provide common built-in error types so devs don't have to always define their own
2 parents a5edc71 + 2296c3b commit db8a7d4

File tree

12 files changed

+1424
-1
lines changed

12 files changed

+1424
-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.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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 {
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+
/// A user-friendly error message suitable for display to end users.
104+
public var userFriendlyMessage: String {
105+
switch self {
106+
case .connectionFailed:
107+
return String(
108+
localized: "BuiltInErrors.DatabaseError.connectionFailed",
109+
defaultValue: "Unable to establish a connection to the database. Check your network settings and try again.",
110+
bundle: .module
111+
)
112+
case .operationFailed(let context):
113+
return String(
114+
localized: "BuiltInErrors.DatabaseError.operationFailed",
115+
defaultValue: "The database operation for \(context) could not be completed. Please retry the action.",
116+
bundle: .module
117+
)
118+
case .recordNotFound(let entity, let identifier):
119+
let idMessage = identifier.map { " with ID \($0)" } ?? ""
120+
return String(
121+
localized: "BuiltInErrors.DatabaseError.recordNotFound",
122+
defaultValue: "The \(entity) record\(idMessage) was not found in the database. Verify the details and try again.",
123+
bundle: .module
124+
)
125+
case .generic(let userFriendlyMessage):
126+
return userFriendlyMessage
127+
}
128+
}
129+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 {
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+
/// A user-friendly error message suitable for display to end users.
99+
public var userFriendlyMessage: String {
100+
switch self {
101+
case .fileNotFound(let fileName):
102+
return String(
103+
localized: "BuiltInErrors.FileError.fileNotFound",
104+
defaultValue: "The file \(fileName) could not be located. Please verify the file path and try again.",
105+
bundle: .module
106+
)
107+
case .readFailed(let fileName):
108+
return String(
109+
localized: "BuiltInErrors.FileError.readError",
110+
defaultValue: "An error occurred while attempting to read the file \(fileName). Please check file permissions and try again.",
111+
bundle: .module
112+
)
113+
case .writeFailed(let fileName):
114+
return String(
115+
localized: "BuiltInErrors.FileError.writeError",
116+
defaultValue: "Unable to write to the file \(fileName). Ensure you have the necessary permissions and try again.",
117+
bundle: .module
118+
)
119+
case .generic(let userFriendlyMessage):
120+
return userFriendlyMessage
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)