Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
36 changes: 35 additions & 1 deletion Sources/ErrorKit/BuiltInErrors/DatabaseError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import Foundation
/// }
/// }
/// ```
public enum DatabaseError: Throwable {
public enum DatabaseError: Throwable, Catching {
/// The database connection failed.
///
/// # Example
Expand Down Expand Up @@ -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 {
Expand All @@ -124,6 +156,8 @@ public enum DatabaseError: Throwable {
)
case .generic(let userFriendlyMessage):
return userFriendlyMessage
case .caught(let error):
return ErrorKit.userFriendlyMessage(for: error)
}
}
}
33 changes: 32 additions & 1 deletion Sources/ErrorKit/BuiltInErrors/FileError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import Foundation
/// }
/// }
/// ```
public enum FileError: Throwable {
public enum FileError: Throwable, Catching {
/// The file could not be found.
///
/// # Example
Expand Down Expand Up @@ -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 {
Expand All @@ -118,6 +147,8 @@ public enum FileError: Throwable {
)
case .generic(let userFriendlyMessage):
return userFriendlyMessage
case .caught(let error):
return ErrorKit.userFriendlyMessage(for: error)
}
}
}
34 changes: 32 additions & 2 deletions Sources/ErrorKit/BuiltInErrors/GenericError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
/// }
/// }
/// }
/// ```
Expand All @@ -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

Expand All @@ -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))
}
}
33 changes: 32 additions & 1 deletion Sources/ErrorKit/BuiltInErrors/NetworkError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import Foundation
/// }
/// }
/// ```
public enum NetworkError: Throwable {
public enum NetworkError: Throwable, Catching {
/// No internet connection is available.
///
/// # Example
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -185,6 +214,8 @@ public enum NetworkError: Throwable {
)
case .generic(let userFriendlyMessage):
return userFriendlyMessage
case .caught(let error):
return ErrorKit.userFriendlyMessage(for: error)
}
}
}
Loading