diff --git a/README.md b/README.md index 67f8ce5..4258244 100644 --- a/README.md +++ b/README.md @@ -293,3 +293,131 @@ When contributing: - Follow the existing naming conventions 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. + + +## Simplified Error Nesting with the `Catching` Protocol + +ErrorKit's `Catching` protocol simplifies error handling in modular applications by providing an elegant way to handle nested error hierarchies. It eliminates the need for explicit wrapper cases while maintaining type safety through typed throws. + +### The Problem with Manual Error Wrapping + +In modular applications, errors often need to be propagated up through multiple layers. The traditional approach requires defining explicit wrapper cases for each possible error type: + +```swift +enum ProfileError: Error { + case validationFailed(field: String) + case databaseError(DatabaseError) // Wrapper case needed + case networkError(NetworkError) // Another wrapper case + case fileError(FileError) // Yet another wrapper +} + +// And manual error wrapping in code: + do { + try database.fetch(id) + } catch let error as DatabaseError { + throw .databaseError(error) + } +``` + +This leads to verbose error types and tedious error handling code when attempting to use typed throws. + +### The Solution: `Catching` Protocol + +ErrorKit's `Catching` protocol provides a single `caught` case that can wrap any error, plus a convenient `catch` function for automatic error wrapping: + +```swift +enum ProfileError: Throwable, Catching { + case validationFailed(field: String) + case caught(Error) // Single case handles all nested errors! +} + +struct ProfileRepository { + func loadProfile(id: String) throws(ProfileError) { + // Regular error throwing for validation + guard id.isValidFormat else { + throw ProfileError.validationFailed(field: "id") + } + + // Automatically wrap any database or file errors + let userData = try ProfileError.catch { + let user = try database.loadUser(id) + let settings = try fileSystem.readUserSettings(user.settingsPath) + return UserProfile(user: user, settings: settings) + } + } +} +``` + +Note the `ProfileError.catch` function call, which wraps any errors into the `caught` case and also passes through the return type. + +### Built-in Support in ErrorKit Types + +All of ErrorKit's built-in error types (`DatabaseError`, `FileError`, `NetworkError`, etc.) already conform to `Catching`, allowing you to easily wrap system errors or other error types: + +```swift +func saveUserData() throws(DatabaseError) { + // Automatically wraps SQLite errors, file system errors, etc. + try DatabaseError.catch { + try database.beginTransaction() + try database.execute(query) + try database.commit() + } +} +``` + +### Adding Catching to Your Error Types + +Making your own error types support automatic error wrapping is simple: + +1. Conform to the `Catching` protocol +2. Add the `caught(Error)` case to your error type +3. Use the `catch` function for automatic wrapping + +```swift +enum AppError: Throwable, Catching { + case invalidConfiguration + case caught(Error) // Required for Catching protocol + + var userFriendlyMessage: String { + switch self { + case .invalidConfiguration: + return String(localized: "The app configuration is invalid.") + case .caught(let error): + return ErrorKit.userFriendlyMessage(for: error) + } + } +} + +// Usage is clean and simple: +func appOperation() throws(AppError) { + // Explicit error throwing for known cases + guard configFileExists else { + throw AppError.invalidConfiguration + } + + // Automatic wrapping for system errors and other error types + try AppError.catch { + try riskyOperation() + try anotherRiskyOperation() + } +} +``` + +### Benefits of Using `Catching` + +- **Less Boilerplate**: No need for explicit wrapper cases for each error type +- **Type Safety**: Maintains typed throws while simplifying error handling +- **Clean Code**: Reduces error handling verbosity +- **Automatic Message Propagation**: User-friendly messages flow through the error chain +- **Easy Integration**: Works seamlessly with existing error types +- **Return Value Support**: The `catch` function preserves return values from wrapped operations + +### Best Practices + +- Use `Catching` for error types that might wrap other errors +- Keep error hierarchies shallow when possible +- Use specific error cases for known errors, `caught` for others +- Preserve user-friendly messages when wrapping errors +- Consider error recovery strategies at each level + +The `Catching` protocol makes error handling in Swift more intuitive and maintainable, especially in larger applications with complex error hierarchies. Combined with typed throws, it provides a powerful way to handle errors while keeping your code clean and maintainable. diff --git a/Sources/ErrorKit/BuiltInErrors/DatabaseError.swift b/Sources/ErrorKit/BuiltInErrors/DatabaseError.swift index 0a963a4..c220f4b 100644 --- a/Sources/ErrorKit/BuiltInErrors/DatabaseError.swift +++ b/Sources/ErrorKit/BuiltInErrors/DatabaseError.swift @@ -34,7 +34,7 @@ import Foundation /// } /// } /// ``` -public enum DatabaseError: Throwable { +public enum DatabaseError: Throwable, Catching { /// The database connection failed. /// /// # Example @@ -100,6 +100,38 @@ public enum DatabaseError: Throwable { /// ``` case generic(userFriendlyMessage: String) + /// Represents a child error (either from your own error types or unknown system errors) that was wrapped into this error type. + /// This case is used internally by the ``catch(_:)`` function to store any errors thrown by the wrapped code. + /// + /// # Example + /// ```swift + /// struct UserRepository { + /// func fetchUserDetails(id: String) throws(DatabaseError) { + /// // Check if user exists - simple case with explicit error + /// guard let user = database.findUser(id: id) else { + /// throw DatabaseError.recordNotFound(entity: "User", identifier: id) + /// } + /// + /// // Any errors from parsing or file access are automatically wrapped + /// let preferences = try DatabaseError.catch { + /// let prefsData = try fileManager.contents(ofFile: user.preferencesPath) + /// return try JSONDecoder().decode(UserPreferences.self, from: prefsData) + /// } + /// + /// // Use the loaded preferences + /// user.applyPreferences(preferences) + /// } + /// } + /// ``` + /// + /// The `caught` case stores the original error while maintaining type safety through typed throws. + /// Instead of manually catching and wrapping unknown errors, use the ``catch(_:)`` function + /// which automatically wraps any thrown errors into this case. + /// + /// - Parameters: + /// - error: The original error that was wrapped into this error type. + case caught(Error) + /// A user-friendly error message suitable for display to end users. public var userFriendlyMessage: String { switch self { @@ -124,6 +156,8 @@ public enum DatabaseError: Throwable { ) case .generic(let userFriendlyMessage): return userFriendlyMessage + case .caught(let error): + return ErrorKit.userFriendlyMessage(for: error) } } } diff --git a/Sources/ErrorKit/BuiltInErrors/FileError.swift b/Sources/ErrorKit/BuiltInErrors/FileError.swift index 15301d3..35c4b90 100644 --- a/Sources/ErrorKit/BuiltInErrors/FileError.swift +++ b/Sources/ErrorKit/BuiltInErrors/FileError.swift @@ -34,7 +34,7 @@ import Foundation /// } /// } /// ``` -public enum FileError: Throwable { +public enum FileError: Throwable, Catching { /// The file could not be found. /// /// # Example @@ -95,6 +95,35 @@ public enum FileError: Throwable { /// ``` case generic(userFriendlyMessage: String) + /// An error that occurred during a file operation, wrapped into this error type using the ``catch(_:)`` function. + /// This could include system-level file errors, encoding/decoding errors, or any other errors encountered during file operations. + /// + /// # Example + /// ```swift + /// struct DocumentStorage { + /// func saveDocument(_ document: Document) throws(FileError) { + /// // Regular error for missing file + /// guard fileExists(document.path) else { + /// throw FileError.fileNotFound(fileName: document.name) + /// } + /// + /// // Automatically wrap encoding and file system errors + /// try FileError.catch { + /// let data = try JSONEncoder().encode(document) + /// try data.write(to: document.url, options: .atomic) + /// } + /// } + /// } + /// ``` + /// + /// The `caught` case stores the original error while maintaining type safety through typed throws. + /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function + /// which automatically wraps any thrown errors into this case. + /// + /// - Parameters: + /// - error: The original error that occurred during the file operation. + case caught(Error) + /// A user-friendly error message suitable for display to end users. public var userFriendlyMessage: String { switch self { @@ -118,6 +147,8 @@ public enum FileError: Throwable { ) case .generic(let userFriendlyMessage): return userFriendlyMessage + case .caught(let error): + return ErrorKit.userFriendlyMessage(for: error) } } } diff --git a/Sources/ErrorKit/BuiltInErrors/GenericError.swift b/Sources/ErrorKit/BuiltInErrors/GenericError.swift index 6957c63..59f94a7 100644 --- a/Sources/ErrorKit/BuiltInErrors/GenericError.swift +++ b/Sources/ErrorKit/BuiltInErrors/GenericError.swift @@ -15,7 +15,11 @@ import Foundation /// userFriendlyMessage: String(localized: "The business data doesn't meet required criteria") /// ) /// } -/// // Continue with business logic +/// +/// // Automatically wrap any other errors if needed +/// try GenericError.catch { +/// try validateAdditionalRules(data) +/// } /// } /// } /// ``` @@ -33,7 +37,7 @@ import Foundation /// } /// } /// ``` -public struct GenericError: Throwable { +public struct GenericError: Throwable, Catching { /// A user-friendly message describing the error. public let userFriendlyMessage: String @@ -56,4 +60,30 @@ public struct GenericError: Throwable { public init(userFriendlyMessage: String) { self.userFriendlyMessage = userFriendlyMessage } + + /// Creates a new generic error that wraps another error. + /// Used internally by the ``catch(_:)`` function to automatically wrap any thrown errors. + /// + /// # Example + /// ```swift + /// struct FileProcessor { + /// func processUserData() throws(GenericError) { + /// // Explicit throwing for validation + /// guard isValidPath(userDataPath) else { + /// throw GenericError(userFriendlyMessage: "Invalid file path selected") + /// } + /// + /// // Automatically wrap any file system or JSON errors + /// let userData = try GenericError.catch { + /// let data = try Data(contentsOf: userDataPath) + /// return try JSONDecoder().decode(UserData.self, from: data) + /// } + /// } + /// } + /// ``` + /// + /// - Parameter error: The error to be wrapped. + public static func caught(_ error: Error) -> Self { + GenericError(userFriendlyMessage: ErrorKit.userFriendlyMessage(for: error)) + } } diff --git a/Sources/ErrorKit/BuiltInErrors/NetworkError.swift b/Sources/ErrorKit/BuiltInErrors/NetworkError.swift index 436a712..2caf238 100644 --- a/Sources/ErrorKit/BuiltInErrors/NetworkError.swift +++ b/Sources/ErrorKit/BuiltInErrors/NetworkError.swift @@ -39,7 +39,7 @@ import Foundation /// } /// } /// ``` -public enum NetworkError: Throwable { +public enum NetworkError: Throwable, Catching { /// No internet connection is available. /// /// # Example @@ -145,6 +145,35 @@ public enum NetworkError: Throwable { /// ``` case generic(userFriendlyMessage: String) + /// An error that occurred during a network operation, wrapped into this error type using the ``catch(_:)`` function. + /// This could include URLSession errors, SSL/TLS errors, or any other errors encountered during network communication. + /// + /// # Example + /// ```swift + /// struct APIClient { + /// func fetchUserProfile(id: String) throws(NetworkError) { + /// // Regular error for no connectivity + /// guard isNetworkReachable else { + /// throw NetworkError.noInternet + /// } + /// + /// // Automatically wrap URLSession and decoding errors + /// let profile = try NetworkError.catch { + /// let (data, response) = try await URLSession.shared.data(from: userProfileURL) + /// return try JSONDecoder().decode(UserProfile.self, from: data) + /// } + /// } + /// } + /// ``` + /// + /// The `caught` case stores the original error while maintaining type safety through typed throws. + /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function + /// which automatically wraps any thrown errors into this case. + /// + /// - Parameters: + /// - error: The original error that occurred during the network operation. + case caught(Error) + /// A user-friendly error message suitable for display to end users. public var userFriendlyMessage: String { switch self { @@ -185,6 +214,8 @@ public enum NetworkError: Throwable { ) case .generic(let userFriendlyMessage): return userFriendlyMessage + case .caught(let error): + return ErrorKit.userFriendlyMessage(for: error) } } } diff --git a/Sources/ErrorKit/BuiltInErrors/OperationError.swift b/Sources/ErrorKit/BuiltInErrors/OperationError.swift index cb6aa5a..890d354 100644 --- a/Sources/ErrorKit/BuiltInErrors/OperationError.swift +++ b/Sources/ErrorKit/BuiltInErrors/OperationError.swift @@ -27,7 +27,7 @@ import Foundation /// } /// } /// ``` -public enum OperationError: Throwable { +public enum OperationError: Throwable, Catching { /// The operation could not start due to a dependency failure. /// /// # Example @@ -74,6 +74,36 @@ public enum OperationError: Throwable { /// ``` case generic(userFriendlyMessage: String) + /// An error that occurred during an operation execution, wrapped into this error type using the ``catch(_:)`` function. + /// This could include task cancellation errors, operation queue errors, or any other errors encountered during complex operations. + /// + /// # Example + /// ```swift + /// struct DataProcessor { + /// func processLargeDataset(_ dataset: Dataset) throws(OperationError) { + /// // Regular error for operation prerequisites + /// guard meetsMemoryRequirements(dataset) else { + /// throw OperationError.dependencyFailed(dependency: "Memory Requirements") + /// } + /// + /// // Automatically wrap operation and processing errors + /// let result = try OperationError.catch { + /// let operation = try ProcessingOperation(dataset) + /// try operation.validateInputs() + /// return try operation.execute() + /// } + /// } + /// } + /// ``` + /// + /// The `caught` case stores the original error while maintaining type safety through typed throws. + /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function + /// which automatically wraps any thrown errors into this case. + /// + /// - Parameters: + /// - error: The original error that occurred during the operation. + case caught(Error) + /// A user-friendly error message suitable for display to end users. public var userFriendlyMessage: String { switch self { @@ -91,6 +121,8 @@ public enum OperationError: Throwable { ) case .generic(let userFriendlyMessage): return userFriendlyMessage + case .caught(let error): + return ErrorKit.userFriendlyMessage(for: error) } } } diff --git a/Sources/ErrorKit/BuiltInErrors/ParsingError.swift b/Sources/ErrorKit/BuiltInErrors/ParsingError.swift index 0afb172..e97c7bc 100644 --- a/Sources/ErrorKit/BuiltInErrors/ParsingError.swift +++ b/Sources/ErrorKit/BuiltInErrors/ParsingError.swift @@ -27,7 +27,7 @@ import Foundation /// } /// } /// ``` -public enum ParsingError: Throwable { +public enum ParsingError: Throwable, Catching { /// The input was invalid and could not be parsed. /// /// # Example @@ -75,6 +75,35 @@ public enum ParsingError: Throwable { /// ``` case generic(userFriendlyMessage: String) + /// An error that occurred during parsing or data transformation, wrapped into this error type using the ``catch(_:)`` function. + /// This could include JSON decoding errors, format validation errors, or any other errors encountered during data parsing. + /// + /// # Example + /// ```swift + /// struct ProfileParser { + /// func parseUserProfile(data: Data) throws(ParsingError) { + /// // Regular error for missing data + /// guard !data.isEmpty else { + /// throw ParsingError.missingField(field: "profile_data") + /// } + /// + /// // Automatically wrap JSON decoding and validation errors + /// let profile = try ParsingError.catch { + /// let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] + /// return try UserProfile(validating: json) + /// } + /// } + /// } + /// ``` + /// + /// The `caught` case stores the original error while maintaining type safety through typed throws. + /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function + /// which automatically wraps any thrown errors into this case. + /// + /// - Parameters: + /// - error: The original error that occurred during the parsing operation. + case caught(Error) + /// A user-friendly error message suitable for display to end users. public var userFriendlyMessage: String { switch self { @@ -92,6 +121,8 @@ public enum ParsingError: Throwable { ) case .generic(let userFriendlyMessage): return userFriendlyMessage + case .caught(let error): + return ErrorKit.userFriendlyMessage(for: error) } } } diff --git a/Sources/ErrorKit/BuiltInErrors/PermissionError.swift b/Sources/ErrorKit/BuiltInErrors/PermissionError.swift index a0fe789..980bfa9 100644 --- a/Sources/ErrorKit/BuiltInErrors/PermissionError.swift +++ b/Sources/ErrorKit/BuiltInErrors/PermissionError.swift @@ -33,7 +33,7 @@ import Foundation /// } /// } /// ``` -public enum PermissionError: Throwable { +public enum PermissionError: Throwable, Catching { /// The user denied the required permission. /// /// # Example @@ -96,6 +96,35 @@ public enum PermissionError: Throwable { /// ``` case generic(userFriendlyMessage: String) + /// An error that occurred during permission handling, wrapped into this error type using the ``catch(_:)`` function. + /// This could include authorization errors, system permission errors, or any other errors encountered during permission requests. + /// + /// # Example + /// ```swift + /// struct MediaAccessManager { + /// func requestMediaAccess() throws(PermissionError) { + /// // Regular error for denied permission + /// guard !isPermissionExplicitlyDenied else { + /// throw PermissionError.denied(permission: "Media Library") + /// } + /// + /// // Automatically wrap authorization and system permission errors + /// try PermissionError.catch { + /// try await AVCaptureDevice.requestAccess(for: .video) + /// try await PHPhotoLibrary.requestAuthorization(for: .readWrite) + /// } + /// } + /// } + /// ``` + /// + /// The `caught` case stores the original error while maintaining type safety through typed throws. + /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function + /// which automatically wraps any thrown errors into this case. + /// + /// - Parameters: + /// - error: The original error that occurred during the permission operation. + case caught(Error) + /// A user-friendly error message suitable for display to end users. public var userFriendlyMessage: String { switch self { @@ -119,6 +148,8 @@ public enum PermissionError: Throwable { ) case .generic(let userFriendlyMessage): return userFriendlyMessage + case .caught(let error): + return ErrorKit.userFriendlyMessage(for: error) } } } diff --git a/Sources/ErrorKit/BuiltInErrors/StateError.swift b/Sources/ErrorKit/BuiltInErrors/StateError.swift index 912d21f..410ac30 100644 --- a/Sources/ErrorKit/BuiltInErrors/StateError.swift +++ b/Sources/ErrorKit/BuiltInErrors/StateError.swift @@ -27,7 +27,7 @@ import Foundation /// } /// } /// ``` -public enum StateError: Throwable { +public enum StateError: Throwable, Catching { /// The required state was not met to proceed with the operation. /// /// # Example @@ -88,6 +88,36 @@ public enum StateError: Throwable { /// ``` case generic(userFriendlyMessage: String) + /// An error that occurred due to state management issues, wrapped into this error type using the ``catch(_:)`` function. + /// This could include state transition errors, validation errors, or any other errors encountered during state-dependent operations. + /// + /// # Example + /// ```swift + /// struct OrderProcessor { + /// func finalizeOrder(_ order: Order) throws(StateError) { + /// // Regular error for invalid state + /// guard order.status == .verified else { + /// throw StateError.invalidState(description: "Order must be verified") + /// } + /// + /// // Automatically wrap payment and inventory state errors + /// try StateError.catch { + /// let paymentResult = try paymentGateway.processPayment(order.payment) + /// try inventoryManager.reserveItems(order.items) + /// try order.moveToState(.finalized) + /// } + /// } + /// } + /// ``` + /// + /// The `caught` case stores the original error while maintaining type safety through typed throws. + /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function + /// which automatically wraps any thrown errors into this case. + /// + /// - Parameters: + /// - error: The original error that occurred during the state-dependent operation. + case caught(Error) + /// A user-friendly error message suitable for display to end users. public var userFriendlyMessage: String { switch self { @@ -111,6 +141,8 @@ public enum StateError: Throwable { ) case .generic(let userFriendlyMessage): return userFriendlyMessage + case .caught(let error): + return ErrorKit.userFriendlyMessage(for: error) } } } diff --git a/Sources/ErrorKit/BuiltInErrors/ValidationError.swift b/Sources/ErrorKit/BuiltInErrors/ValidationError.swift index 4e3d852..c4ef2bc 100644 --- a/Sources/ErrorKit/BuiltInErrors/ValidationError.swift +++ b/Sources/ErrorKit/BuiltInErrors/ValidationError.swift @@ -34,7 +34,7 @@ import Foundation /// } /// } /// ``` -public enum ValidationError: Throwable { +public enum ValidationError: Throwable, Catching { /// The input provided is invalid. /// /// # Example @@ -102,6 +102,36 @@ public enum ValidationError: Throwable { /// ``` case generic(userFriendlyMessage: String) + /// An error that occurred during validation, wrapped into this error type using the ``catch(_:)`` function. + /// This could include data validation errors, format validation errors, or any other errors encountered during validation checks. + /// + /// # Example + /// ```swift + /// struct UserProfileValidator { + /// func validateProfile(_ profile: UserProfile) throws(ValidationError) { + /// // Regular error for field validation + /// guard !profile.name.isEmpty else { + /// throw ValidationError.missingField(field: "Name") + /// } + /// + /// // Automatically wrap complex validation errors + /// try ValidationError.catch { + /// try emailValidator.validateEmailFormat(profile.email) + /// try addressValidator.validateAddress(profile.address) + /// try customFieldsValidator.validate(profile.customFields) + /// } + /// } + /// } + /// ``` + /// + /// The `caught` case stores the original error while maintaining type safety through typed throws. + /// Instead of manually catching and wrapping system errors, use the ``catch(_:)`` function + /// which automatically wraps any thrown errors into this case. + /// + /// - Parameters: + /// - error: The original error that occurred during the validation operation. + case caught(Error) + /// A user-friendly error message suitable for display to end users. public var userFriendlyMessage: String { switch self { @@ -125,6 +155,8 @@ public enum ValidationError: Throwable { ) case .generic(let userFriendlyMessage): return userFriendlyMessage + case .caught(let error): + return ErrorKit.userFriendlyMessage(for: error) } } } diff --git a/Sources/ErrorKit/Catching.swift b/Sources/ErrorKit/Catching.swift new file mode 100644 index 0000000..5e4a50a --- /dev/null +++ b/Sources/ErrorKit/Catching.swift @@ -0,0 +1,142 @@ +/// A protocol built for typed throws that enables automatic error wrapping for nested error hierarchies through a `caught` case. +/// This simplifies error handling in modular applications where errors need to be propagated up through multiple layers. +/// +/// # Overview +/// When working with nested error types in a modular application, you often need to wrap errors from lower-level +/// modules into higher-level error types. This protocol provides a convenient way to handle such error wrapping +/// without manually defining wrapper cases for each possible error type. +/// +/// # Example +/// Consider an app with profile management that uses both database and file operations: +/// ```swift +/// // Lower-level error types +/// enum DatabaseError: Throwable, Catching { +/// case connectionFailed +/// case recordNotFound(entity: String, identifier: String?) +/// case caught(Error) // Wraps any other database-related errors +/// } +/// +/// enum FileError: Throwable, Catching { +/// case notFound(path: String) +/// case accessDenied(path: String) +/// case caught(Error) // Wraps any other file system errors +/// } +/// +/// // Higher-level error type +/// enum ProfileError: Throwable, Catching { +/// case validationFailed(field: String) +/// case caught(Error) // Automatically wraps both DatabaseError and FileError +/// } +/// +/// struct ProfileRepository { +/// func loadProfile(id: String) throws(ProfileError) { +/// // Explicit error for validation +/// guard id.isValidFormat else { +/// throw ProfileError.validationFailed(field: "id") +/// } +/// +/// // Automatically wrap any database or file errors +/// let userData = try ProfileError.catch { +/// let user = try database.loadUser(id) +/// let settings = try fileSystem.readUserSettings(user.settingsPath) +/// return UserProfile(user: user, settings: settings) +/// } +/// +/// // Use the loaded data +/// self.currentProfile = userData +/// } +/// } +/// ``` +/// +/// Without Catching protocol, you would need explicit cases and manual mapping: +/// ```swift +/// enum ProfileError: Throwable { +/// case validationFailed(field: String) +/// case databaseError(DatabaseError) // Extra case needed +/// case fileError(FileError) // Extra case needed +/// } +/// +/// struct ProfileRepository { +/// func loadProfile(id: String) throws(ProfileError) { +/// guard id.isValidFormat else { +/// throw ProfileError.validationFailed(field: "id") +/// } +/// +/// // Manual error mapping needed for each error type +/// do { +/// let user = try database.loadUser(id) +/// // Nested try-catch needed +/// do { +/// let settings = try fileSystem.readUserSettings(user.settingsPath) +/// self.currentProfile = UserProfile(user: user, settings: settings) +/// } catch let error as FileError { +/// throw .fileError(error) +/// } +/// } catch let error as DatabaseError { +/// throw .databaseError(error) +/// } +/// } +/// } +/// ``` +/// +/// # Benefits +/// - Simplified error type definitions with a single catch-all case +/// - Automatic wrapping of any error type without manual case mapping +/// - Maintained type safety through typed throws +/// - Clean, readable error handling code +/// - Easy propagation of errors through multiple layers +/// - Transparent handling of return values from wrapped operations +public protocol Catching { + /// Creates an instance of this error type that wraps another error. + /// Used internally by the ``catch(_:)`` function to automatically wrap any thrown errors. + /// + /// - Parameter error: The error to be wrapped in this error type. + static func caught(_ error: Error) -> Self +} + +extension Catching { + /// Executes a throwing operation and automatically wraps any thrown errors into this error type's `caught` case, + /// while passing through the operation's return value on success. Great for functions using typed throws. + /// + /// # Overview + /// This function provides a convenient way to: + /// - Execute throwing operations + /// - Automatically wrap any errors into the current error type + /// - Pass through return values from the wrapped code + /// - Maintain type safety with typed throws + /// + /// # Example + /// ```swift + /// struct ProfileRepository { + /// func loadProfile(id: String) throws(ProfileError) { + /// // Regular error throwing for validation + /// guard id.isValidFormat else { + /// throw ProfileError.validationFailed(field: "id") + /// } + /// + /// // Automatically wrap any database or file errors while handling return value + /// let userData = try ProfileError.catch { + /// let user = try database.loadUser(id) + /// let settings = try fileSystem.readUserSettings(user.settingsPath) + /// return UserProfile(user: user, settings: settings) + /// } + /// + /// // Use the loaded data + /// self.currentProfile = userData + /// } + /// } + /// ``` + /// + /// - Parameter operation: The throwing operation to execute. + /// - Returns: The value returned by the operation if successful. + /// - Throws: An instance of `Self` with the original error wrapped in the `caught` case. + public static func `catch`( + _ operation: () throws -> ReturnType + ) throws(Self) -> ReturnType { + do { + return try operation() + } catch { + throw Self.caught(error) + } + } +} diff --git a/Sources/ErrorKit/Resources/Localizable.xcstrings b/Sources/ErrorKit/Resources/Localizable.xcstrings index 4b2ef21..c23e6eb 100644 --- a/Sources/ErrorKit/Resources/Localizable.xcstrings +++ b/Sources/ErrorKit/Resources/Localizable.xcstrings @@ -7,7 +7,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Failed to connect to the database. Please try again later." + "value" : "Unable to establish a connection to the database. Check your network settings and try again." } } } @@ -18,7 +18,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "An error occurred while performing the operation: %@. Please try again." + "value" : "The database operation for %@ could not be completed. Please retry the action." } } } @@ -29,7 +29,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The %1$@ record could not be found.%2$@ Please check and try again." + "value" : "The %1$@ record%2$@ was not found in the database. Verify the details and try again." } } } @@ -40,7 +40,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The file %@ could not be found. Please check the file path." + "value" : "The file %@ could not be located. Please verify the file path and try again." } } } @@ -51,7 +51,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "There was an issue reading the file %@. Please try again." + "value" : "An error occurred while attempting to read the file %@. Please check file permissions and try again." } } } @@ -62,7 +62,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "There was an issue writing to the file %@. Please try again." + "value" : "Unable to write to the file %@. Ensure you have the necessary permissions and try again." } } } @@ -73,7 +73,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The request was malformed (%1$lld): %2$@. Please review and try again." + "value" : "There was an issue with the request (Code: %1$lld). %2$@. Please review and retry." } } } @@ -84,7 +84,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The data received from the server could not be processed. Please try again." + "value" : "Unable to process the server's response. Please try again or contact support if the issue persists." } } } @@ -95,7 +95,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "No internet connection is available. Please check your network settings and try again." + "value" : "Unable to connect to the internet. Please check your network settings and try again." } } } @@ -106,7 +106,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The server encountered an error (Code: %lld)." + "value" : "The server encountered an error (Code: %lld). " } } } @@ -117,7 +117,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The request timed out. Please try again later." + "value" : "The network request took too long to complete. Please check your connection and try again." } } } @@ -128,7 +128,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The operation was canceled. Please try again if necessary." + "value" : "The operation was canceled at your request. You can retry the action if needed." } } } @@ -139,18 +139,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The operation could not be completed due to a failed dependency: %@." - } - } - } - }, - "BuiltInErrors.OperationError.unknownFailure" : { - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "The operation failed due to an unknown reason: %@. Please try again or contact support." + "value" : "The operation could not be started because a required component failed to initialize: %@. Please restart the application or contact support." } } } @@ -161,7 +150,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The provided input is invalid: %@. Please correct it and try again." + "value" : "The provided input could not be processed correctly: %@. Please review the input and ensure it matches the expected format." } } } @@ -172,7 +161,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "A required field is missing: %@. Please review and try again." + "value" : "The required information is incomplete. The %@ field is missing and must be provided to continue." } } } @@ -183,7 +172,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The %@ permission was denied. Please enable it in Settings to continue." + "value" : "Access to %@ was declined. To use this feature, please enable the permission in your device Settings." } } } @@ -194,7 +183,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The %@ permission has not been determined. Please try again or check your Settings." + "value" : "Permission for %@ has not been confirmed. Please review and grant access in your device Settings." } } } @@ -205,7 +194,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The %@ permission is restricted. This may be due to parental controls or other system restrictions." + "value" : "Access to %@ is currently restricted. This may be due to system settings or parental controls." } } } @@ -216,7 +205,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The operation cannot be performed because the state is already finalized." + "value" : "This item has already been finalized and cannot be modified. Please create a new version if changes are needed." } } } @@ -227,7 +216,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The operation cannot proceed due to an invalid state: %@." + "value" : "The current state prevents this action: %@. Please ensure all requirements are met and try again." } } } @@ -238,7 +227,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "A required condition was not met: %@. Please review and try again." + "value" : "A required condition was not met: %@. Please complete all prerequisites before proceeding." } } } @@ -249,7 +238,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "%1$@ exceeds the maximum allowed length of %2$lld characters. Please shorten it." + "value" : "The %1$@ field cannot be longer than %2$lld characters. Please shorten your input and try again." } } } @@ -260,7 +249,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "The value provided for %@ is invalid. Please correct it." + "value" : "The value entered for %@ is not in the correct format. Please review the requirements and try again." } } } @@ -271,7 +260,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "%@ is a required field. Please provide a value." + "value" : "Please provide a value for %@. This information is required to proceed." } } }