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
14 changes: 14 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# EditorConfig is awesome: https://editorconfig.org
root = true

[*]

indent_style = space
tab_width = 6
indent_size = 3

end_of_line = lf
insert_final_newline = true

max_line_length = 160
trim_trailing_whitespace = true
9 changes: 0 additions & 9 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@ on:
pull_request:

jobs:
test-linux:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Run tests
run: swift test

test-macos:
runs-on: macos-15

Expand Down
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import PackageDescription

let package = Package(
name: "ErrorKit",
defaultLocalization: "en",
platforms: [.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v16)],
products: [.library(name: "ErrorKit", targets: ["ErrorKit"])],
targets: [
.target(name: "ErrorKit"),
.target(
name: "ErrorKit",
resources: [.process("Resources/Localizable.xcstrings")]
),
.testTarget(name: "ErrorKitTests", dependencies: ["ErrorKit"]),
]
)
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,44 @@ 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.


## Enhanced Error Descriptions with `enhancedDescription(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.

### 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.

### 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:

```swift
do {
// Attempt a network request
let url = URL(string: "https://example.com")!
let _ = try Data(contentsOf: url)
} catch {
// Print or show the enhanced error message to a user
print(ErrorKit.enhancedDescription(for: error))
// Example output: "You are not connected to the Internet. Please check your connection."
}
```

### Why Use `enhancedDescription(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.
- **Community Contributions**: The descriptions are regularly improved by the developer community. If you encounter a new or unexpected error, feel free to contribute by submitting a pull request.

### Contribution Welcome!

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.
63 changes: 63 additions & 0 deletions Sources/ErrorKit/CommonErrors/ErrorKit+CoreData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#if canImport(CoreData)
import CoreData
#endif

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

if nsError.domain == NSCocoaErrorDomain {
switch nsError.code {

case NSPersistentStoreSaveError:
return String(
localized: "CommonErrors.CoreData.NSPersistentStoreSaveError",
defaultValue: "Failed to save the data. Please try again.",
bundle: .module
)
case NSValidationMultipleErrorsError:
return String(
localized: "CommonErrors.CoreData.NSValidationMultipleErrorsError",
defaultValue: "Multiple validation errors occurred while saving.",
bundle: .module
)
case NSValidationMissingMandatoryPropertyError:
return String(
localized: "CommonErrors.CoreData.NSValidationMissingMandatoryPropertyError",
defaultValue: "A mandatory property is missing. Please fill all required fields.",
bundle: .module
)
case NSValidationRelationshipLacksMinimumCountError:
return String(
localized: "CommonErrors.CoreData.NSValidationRelationshipLacksMinimumCountError",
defaultValue: "A relationship is missing required related objects.",
bundle: .module
)
case NSPersistentStoreIncompatibleVersionHashError:
return String(
localized: "CommonErrors.CoreData.NSPersistentStoreIncompatibleVersionHashError",
defaultValue: "The data store is incompatible with the current model version.",
bundle: .module
)
case NSPersistentStoreOpenError:
return String(
localized: "CommonErrors.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",
defaultValue: "An object validation error occurred.",
bundle: .module
)
default:
return nil
}
}
#endif

return nil
}
}
107 changes: 107 additions & 0 deletions Sources/ErrorKit/CommonErrors/ErrorKit+Foundation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

extension ErrorKit {
static func enhancedFoundationDescription(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",
defaultValue: "You are not connected to the Internet. Please check your connection.",
bundle: .module
)
case .timedOut:
return String(
localized: "CommonErrors.URLError.timedOut",
defaultValue: "The request timed out. Please try again later.",
bundle: .module
)
case .cannotFindHost:
return String(
localized: "CommonErrors.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",
defaultValue: "The network connection was lost. Please try again.",
bundle: .module
)
default:
return String(
localized: "CommonErrors.URLError.default",
defaultValue: "A network error occurred: \(urlError.localizedDescription)",
bundle: .module
)
}

// CocoaError: File-related errors
case let cocoaError as CocoaError:
switch cocoaError.code {
case .fileNoSuchFile:
return String(
localized: "CommonErrors.CocoaError.fileNoSuchFile",
defaultValue: "The file could not be found.",
bundle: .module
)
case .fileReadNoPermission:
return String(
localized: "CommonErrors.CocoaError.fileReadNoPermission",
defaultValue: "You do not have permission to read this file.",
bundle: .module
)
case .fileWriteOutOfSpace:
return String(
localized: "CommonErrors.CocoaError.fileWriteOutOfSpace",
defaultValue: "There is not enough disk space to complete the operation.",
bundle: .module
)
default:
return String(
localized: "CommonErrors.CocoaError.default",
defaultValue: "A file system error occurred: \(cocoaError.localizedDescription)",
bundle: .module
)
}

// POSIXError: POSIX errors
case let posixError as POSIXError:
switch posixError.code {
case .ENOSPC:
return String(
localized: "CommonErrors.POSIXError.ENOSPC",
defaultValue: "There is no space left on the device.",
bundle: .module
)
case .EACCES:
return String(
localized: "CommonErrors.POSIXError.EACCES",
defaultValue: "Permission denied. Please check your file permissions.",
bundle: .module
)
case .EBADF:
return String(
localized: "CommonErrors.POSIXError.EBADF",
defaultValue: "Bad file descriptor. The file may be closed or invalid.",
bundle: .module
)
default:
return String(
localized: "CommonErrors.POSIXError.default",
defaultValue: "A system error occurred: \(posixError.localizedDescription)",
bundle: .module
)
}

default:
return nil
}
}
}
52 changes: 52 additions & 0 deletions Sources/ErrorKit/CommonErrors/ErrorKit+MapKit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#if canImport(MapKit)
import MapKit
#endif

extension ErrorKit {
static func enhancedMapKitDescription(for error: Error) -> String? {
#if canImport(MapKit)
if let mkError = error as? MKError {
switch mkError.code {
case .unknown:
return String(
localized: "CommonErrors.MKError.unknown",
defaultValue: "An unknown error occurred in MapKit.",
bundle: .module
)
case .serverFailure:
return String(
localized: "CommonErrors.MKError.serverFailure",
defaultValue: "The MapKit server returned an error. Please try again later.",
bundle: .module
)
case .loadingThrottled:
return String(
localized: "CommonErrors.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",
defaultValue: "The requested placemark could not be found. Please check the location details.",
bundle: .module
)
case .directionsNotFound:
return String(
localized: "CommonErrors.MKError.directionsNotFound",
defaultValue: "No directions could be found for the specified route.",
bundle: .module
)
default:
return String(
localized: "CommonErrors.MKError.default",
defaultValue: "A MapKit error occurred: \(mkError.localizedDescription)",
bundle: .module
)
}
}
#endif

return nil
}
}
57 changes: 56 additions & 1 deletion Sources/ErrorKit/ErrorKit.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,58 @@
import Foundation

#warning("🚧 not yet implemented")
public enum ErrorKit {
/// Provides enhanced, user-friendly, localized error descriptions for a wide range of system errors.
///
/// This function analyzes the given `Error` and returns a clearer, more helpful message than the default system-provided description.
/// All descriptions are localized, ensuring that users receive messages in their preferred language where available.
///
/// The list of enhanced descriptions is maintained and regularly improved by the developer community. Contributions are welcome—if you find bugs or encounter new errors, feel free to submit a pull request (PR) for review.
///
/// Errors from various domains, such as `Foundation`, `CoreData`, `MapKit`, and more, are supported. As the project evolves, additional domains may be included to ensure comprehensive coverage.
///
/// - Parameter error: The `Error` instance for which an enhanced description is needed.
/// - Returns: A `String` containing an enhanced, localized, user-readable error message.
///
/// ## Usage Example:
/// ```swift
/// do {
/// // Example network request
/// let url = URL(string: "https://example.com")!
/// let _ = try Data(contentsOf: url)
/// } catch {
/// print(ErrorKit.enhancedDescription(for: error))
/// // Output: "You are not connected to the Internet. Please check your connection." (if applicable)
/// }
/// ```
public static func enhancedDescription(for error: Error) -> String {
// Any types conforming to `Throwable` are assumed to already have a good description
if let throwable = error as? Throwable {
return throwable.localizedDescription
}

if let foundationDescription = Self.enhancedFoundationDescription(for: error) {
return foundationDescription
}

if let coreDataDescription = Self.enhancedCoreDataDescription(for: error) {
return coreDataDescription
}

if let mapKitDescription = Self.enhancedMapKitDescription(for: error) {
return mapKitDescription
}

// LocalizedError: The recommended error type to conform to in Swift by default.
if let localizedError = error as? LocalizedError {
return [
localizedError.errorDescription,
localizedError.failureReason,
localizedError.recoverySuggestion,
].compactMap(\.self).joined(separator: " ")
}

// Default fallback (adds domain & code at least)
let nsError = error as NSError
return "[\(nsError.domain): \(nsError.code)] \(nsError.localizedDescription)"
}
}
Loading