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
79 changes: 68 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ The correct approach is to conform to `LocalizedError`, which defines the follow
- `recoverySuggestion: String?`
- `helpAnchor: String?`

However, since all of these properties are optional, you won’t get any compiler errors if you forget to implement them. Worse, only `errorDescription` affects `localizedDescription`. Fields like `failureReason` and `recoverySuggestion` are ignored, while `helpAnchor` is rarely used today.
However, since all of these properties are optional, you won’t get any compiler errors if you forget to implement them. Worse, only `errorDescription` affects `localizedDescription`. Fields like `failureReason` and `recoverySuggestion` are ignored, while `helpAnchor` is rarely used nowadays.

This makes `LocalizedError` both confusing and error-prone.

Expand All @@ -71,11 +71,11 @@ To address these issues, **ErrorKit** introduces the `Throwable` protocol:

```swift
public protocol Throwable: LocalizedError {
var localizedDescription: String { get }
var userFriendlyMessage: String { get }
}
```

This protocol is simple and clear. It’s named `Throwable` to align with Swift’s `throw` keyword and follows Swift’s convention of using the `able` suffix (like `Codable` and `Identifiable`). Most importantly, it requires the `localizedDescription` property, ensuring your errors behave exactly as expected.
This protocol is simple and clear. It’s named `Throwable` to align with Swift’s `throw` keyword and follows Swift’s convention of using the `able` suffix (like `Codable` and `Identifiable`). Most importantly, it requires the `userFriendlyMessage` property, ensuring your errors behave exactly as expected.

Here’s how you use it:

Expand All @@ -84,7 +84,7 @@ enum NetworkError: Throwable {
case noConnectionToServer
case parsingFailed

var localizedDescription: String {
var userFriendlyMessage: String {
switch self {
case .noConnectionToServer: "Unable to connect to the server."
case .parsingFailed: "Data parsing failed."
Expand All @@ -110,24 +110,24 @@ This approach eliminates boilerplate code while keeping the error definitions co

### Summary

> Conform your custom error types to `Throwable` instead of `Error` or `LocalizedError`. The `Throwable` protocol requires only `localizedDescription: String`, ensuring your error messages are exactly what you expect – no surprises.
> Conform your custom error types to `Throwable` instead of `Error` or `LocalizedError`. The `Throwable` protocol requires only `userFriendlyMessage: String`, ensuring your error messages are exactly what you expect – no surprises.


## Enhanced Error Descriptions with `enhancedDescription(for:)`
## Enhanced Error Descriptions with `userFriendlyMessage(for:)`

ErrorKit goes beyond simplifying error handling — it enhances the clarity of error messages by providing improved, localized descriptions. With the `ErrorKit.enhancedDescription(for:)` function, developers can deliver clear, user-friendly error messages tailored to their audience.
ErrorKit goes beyond simplifying error handling — it enhances the clarity of error messages by providing improved, localized descriptions. With the `ErrorKit.userFriendlyMessage(for:)` function, developers can deliver clear, user-friendly error messages tailored to their audience.

### How It Works

The `enhancedDescription(for:)` function analyzes the provided `Error` and returns an enhanced, localized message. It draws on a community-maintained collection of descriptions to ensure the messages are accurate, helpful, and continuously evolving.
The `userFriendlyMessage(for:)` function analyzes the provided `Error` and returns an enhanced, localized message. It draws on a community-maintained collection of descriptions to ensure the messages are accurate, helpful, and continuously evolving.

### Supported Error Domains

ErrorKit supports errors from various domains such as `Foundation`, `CoreData`, `MapKit`, and more. These domains are continuously updated, providing coverage for the most common error types in Swift development.

### Usage Example

Here’s how to use `enhancedDescription(for:)` to handle errors gracefully:
Here’s how to use `userFriendlyMessage(for:)` to handle errors gracefully:

```swift
do {
Expand All @@ -136,12 +136,12 @@ do {
let _ = try Data(contentsOf: url)
} catch {
// Print or show the enhanced error message to a user
print(ErrorKit.enhancedDescription(for: error))
print(ErrorKit.userFriendlyMessage(for: error))
// Example output: "You are not connected to the Internet. Please check your connection."
}
```

### Why Use `enhancedDescription(for:)`?
### Why Use `userFriendlyMessage(for:)`?

- **Localization**: Error messages are localized to ~40 languages to provide a better user experience.
- **Clarity**: Returns clear and concise error messages, avoiding cryptic system-generated descriptions.
Expand All @@ -152,3 +152,60 @@ do {
Found a bug or missing description? We welcome your contributions! Submit a pull request (PR), and we’ll gladly review and merge it to enhance the library further.

> **Note:** The enhanced error descriptions are constantly evolving, and we’re committed to making them as accurate and helpful as possible.

## Overloads of Common System Functions with Typed Throws

ErrorKit introduces typed-throws overloads for common system APIs like `FileManager` and `URLSession`, providing more granular error handling and improved code clarity. These overloads allow you to handle specific error scenarios with tailored responses, making your code more robust and easier to maintain.

To streamline discovery, ErrorKit uses the same API names prefixed with `throwable`. These functions throw specific errors that conform to `Throwable`, allowing for clear and informative error messages.

**Enhanced User-Friendly Error Messages:**

One of the key advantages of ErrorKit's typed throws is the improved `localizedDescription` property. This property provides user-friendly error messages that are tailored to the specific error type. This eliminates the need for manual error message construction and ensures a consistent and informative user experience.

**Example: Creating a Directory**

```swift
do {
try FileManager.default.throwableCreateDirectory(at: URL(string: "file:///path/to/directory")!)
} catch {
switch error {
case FileManagerError.noWritePermission:
// Request write permission from the user intead of showing error message
default:
// Common error cases have a more descriptive message
showErrorDialog(error.localizedDescription)
}
}
```

The code demonstrates how to handle errors for specific error cases with an improved UX rather than just showing an error message to the user, which can still be the fallback. And the error cases are easy to discover thanks to the typed enum error.

**Example: Handling network request errors**

```swift
do {
let (data, response) = try await URLSession.shared.throwableData(from: URL(string: "https://api.example.com/data")!)
// Process the data and response
} catch {
// Error is of type `URLSessionError`
print(error.localizedDescription)

switch error {
case .timeout, .requestTimeout, .tooManyRequests:
// Automatically retry the request with a backoff strategy
case .noNetwork:
// Show an SF Symbol indicating the user is offline plus a retry button
case .unauthorized:
// Redirect the user to your login-flow (e.g. because token expired)
default:
// Fall back to showing error message
}
}
```

Here, the code leverages the specific error types to implement various kinds of custom logic. This demonstrates the power of typed throws in providing fine-grained control over error handling.

### Summary

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.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import CoreData
#endif

extension ErrorKit {
static func enhancedCoreDataDescription(for error: Error) -> String? {
static func userFriendlyCoreDataMessage(for error: Error) -> String? {
#if canImport(CoreData)
let nsError = error as NSError

Expand All @@ -12,43 +12,43 @@ extension ErrorKit {

case NSPersistentStoreSaveError:
return String(
localized: "CommonErrors.CoreData.NSPersistentStoreSaveError",
localized: "EnhancedDescriptions.CoreData.NSPersistentStoreSaveError",
defaultValue: "Failed to save the data. Please try again.",
bundle: .module
)
case NSValidationMultipleErrorsError:
return String(
localized: "CommonErrors.CoreData.NSValidationMultipleErrorsError",
localized: "EnhancedDescriptions.CoreData.NSValidationMultipleErrorsError",
defaultValue: "Multiple validation errors occurred while saving.",
bundle: .module
)
case NSValidationMissingMandatoryPropertyError:
return String(
localized: "CommonErrors.CoreData.NSValidationMissingMandatoryPropertyError",
localized: "EnhancedDescriptions.CoreData.NSValidationMissingMandatoryPropertyError",
defaultValue: "A mandatory property is missing. Please fill all required fields.",
bundle: .module
)
case NSValidationRelationshipLacksMinimumCountError:
return String(
localized: "CommonErrors.CoreData.NSValidationRelationshipLacksMinimumCountError",
localized: "EnhancedDescriptions.CoreData.NSValidationRelationshipLacksMinimumCountError",
defaultValue: "A relationship is missing required related objects.",
bundle: .module
)
case NSPersistentStoreIncompatibleVersionHashError:
return String(
localized: "CommonErrors.CoreData.NSPersistentStoreIncompatibleVersionHashError",
localized: "EnhancedDescriptions.CoreData.NSPersistentStoreIncompatibleVersionHashError",
defaultValue: "The data store is incompatible with the current model version.",
bundle: .module
)
case NSPersistentStoreOpenError:
return String(
localized: "CommonErrors.CoreData.NSPersistentStoreOpenError",
localized: "EnhancedDescriptions.CoreData.NSPersistentStoreOpenError",
defaultValue: "Unable to open the persistent store. Please check your storage or permissions.",
bundle: .module
)
case NSManagedObjectValidationError:
return String(
localized: "CommonErrors.CoreData.NSManagedObjectValidationError",
localized: "EnhancedDescriptions.CoreData.NSManagedObjectValidationError",
defaultValue: "An object validation error occurred.",
bundle: .module
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,39 @@ import FoundationNetworking
#endif

extension ErrorKit {
static func enhancedFoundationDescription(for error: Error) -> String? {
static func userFriendlyFoundationMessage(for error: Error) -> String? {
switch error {

// URLError: Networking errors
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet:
return String(
localized: "CommonErrors.URLError.notConnectedToInternet",
localized: "EnhancedDescriptions.URLError.notConnectedToInternet",
defaultValue: "You are not connected to the Internet. Please check your connection.",
bundle: .module
)
case .timedOut:
return String(
localized: "CommonErrors.URLError.timedOut",
localized: "EnhancedDescriptions.URLError.timedOut",
defaultValue: "The request timed out. Please try again later.",
bundle: .module
)
case .cannotFindHost:
return String(
localized: "CommonErrors.URLError.cannotFindHost",
localized: "EnhancedDescriptions.URLError.cannotFindHost",
defaultValue: "Unable to find the server. Please check the URL or your network.",
bundle: .module
)
case .networkConnectionLost:
return String(
localized: "CommonErrors.URLError.networkConnectionLost",
localized: "EnhancedDescriptions.URLError.networkConnectionLost",
defaultValue: "The network connection was lost. Please try again.",
bundle: .module
)
default:
return String(
localized: "CommonErrors.URLError.default",
localized: "EnhancedDescriptions.URLError.default",
defaultValue: "A network error occurred: \(urlError.localizedDescription)",
bundle: .module
)
Expand All @@ -47,25 +47,25 @@ extension ErrorKit {
switch cocoaError.code {
case .fileNoSuchFile:
return String(
localized: "CommonErrors.CocoaError.fileNoSuchFile",
localized: "EnhancedDescriptions.CocoaError.fileNoSuchFile",
defaultValue: "The file could not be found.",
bundle: .module
)
case .fileReadNoPermission:
return String(
localized: "CommonErrors.CocoaError.fileReadNoPermission",
localized: "EnhancedDescriptions.CocoaError.fileReadNoPermission",
defaultValue: "You do not have permission to read this file.",
bundle: .module
)
case .fileWriteOutOfSpace:
return String(
localized: "CommonErrors.CocoaError.fileWriteOutOfSpace",
localized: "EnhancedDescriptions.CocoaError.fileWriteOutOfSpace",
defaultValue: "There is not enough disk space to complete the operation.",
bundle: .module
)
default:
return String(
localized: "CommonErrors.CocoaError.default",
localized: "EnhancedDescriptions.CocoaError.default",
defaultValue: "A file system error occurred: \(cocoaError.localizedDescription)",
bundle: .module
)
Expand All @@ -76,25 +76,25 @@ extension ErrorKit {
switch posixError.code {
case .ENOSPC:
return String(
localized: "CommonErrors.POSIXError.ENOSPC",
localized: "EnhancedDescriptions.POSIXError.ENOSPC",
defaultValue: "There is no space left on the device.",
bundle: .module
)
case .EACCES:
return String(
localized: "CommonErrors.POSIXError.EACCES",
localized: "EnhancedDescriptions.POSIXError.EACCES",
defaultValue: "Permission denied. Please check your file permissions.",
bundle: .module
)
case .EBADF:
return String(
localized: "CommonErrors.POSIXError.EBADF",
localized: "EnhancedDescriptions.POSIXError.EBADF",
defaultValue: "Bad file descriptor. The file may be closed or invalid.",
bundle: .module
)
default:
return String(
localized: "CommonErrors.POSIXError.default",
localized: "EnhancedDescriptions.POSIXError.default",
defaultValue: "A system error occurred: \(posixError.localizedDescription)",
bundle: .module
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,43 @@ import MapKit
#endif

extension ErrorKit {
static func enhancedMapKitDescription(for error: Error) -> String? {
static func userFriendlyMapKitMessage(for error: Error) -> String? {
#if canImport(MapKit)
if let mkError = error as? MKError {
switch mkError.code {
case .unknown:
return String(
localized: "CommonErrors.MKError.unknown",
localized: "EnhancedDescriptions.MKError.unknown",
defaultValue: "An unknown error occurred in MapKit.",
bundle: .module
)
case .serverFailure:
return String(
localized: "CommonErrors.MKError.serverFailure",
localized: "EnhancedDescriptions.MKError.serverFailure",
defaultValue: "The MapKit server returned an error. Please try again later.",
bundle: .module
)
case .loadingThrottled:
return String(
localized: "CommonErrors.MKError.loadingThrottled",
localized: "EnhancedDescriptions.MKError.loadingThrottled",
defaultValue: "Map loading is being throttled. Please wait a moment and try again.",
bundle: .module
)
case .placemarkNotFound:
return String(
localized: "CommonErrors.MKError.placemarkNotFound",
localized: "EnhancedDescriptions.MKError.placemarkNotFound",
defaultValue: "The requested placemark could not be found. Please check the location details.",
bundle: .module
)
case .directionsNotFound:
return String(
localized: "CommonErrors.MKError.directionsNotFound",
localized: "EnhancedDescriptions.MKError.directionsNotFound",
defaultValue: "No directions could be found for the specified route.",
bundle: .module
)
default:
return String(
localized: "CommonErrors.MKError.default",
localized: "EnhancedDescriptions.MKError.default",
defaultValue: "A MapKit error occurred: \(mkError.localizedDescription)",
bundle: .module
)
Expand Down
Loading