diff --git a/README.md b/README.md index 250fd88..638cda0 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,23 @@ # ErrorKit -ErrorKit makes error handling in Swift more intuitive. It reduces boilerplate code while providing clearer insights into errors - helpful for users, fun for developers! +Making error handling in Swift more intuitive and powerful with clearer messages, type safety, and user-friendly diagnostics. -## Table of Contents -- [The Problem with Swift's Error Protocol](#the-problem-with-swifts-error-protocol) -- [The Throwable Protocol Solution](#the-throwable-protocol-solution) -- [Built-in Error Types](#built-in-error-types) -- [Enhanced Error Descriptions](#enhanced-error-descriptions) -- [Typed Throws for System Functions](#typed-throws-for-system-functions) -- [Error Nesting with Catching](#error-nesting-with-catching) -- [Error Chain Debugging](#error-chain-debugging) -- [User Feedback with Error Logs](#user-feedback-with-error-logs) +## Overview -## The Problem with Swift's Error Protocol +Swift's error handling has several limitations that make it challenging to create robust, user-friendly applications: +- The `Error` protocol's confusing behavior with `localizedDescription` +- Hard-to-understand system error messages +- Limited type safety in error propagation +- Difficulties with error chain debugging (relevant for typed throws!) +- Challenges in collecting meaningful feedback from users -Swift's `Error` protocol is simple – too simple. While it has no requirements, it provides a computed property `localizedDescription` that's commonly used for logging errors and displaying messages to users. However, this simplicity leads to unexpected behavior and confusion. +ErrorKit addresses these challenges with a suite of lightweight, interconnected features you can adopt progressively. -Consider this example of providing a `localizedDescription` for an error enum: +## Core Features -```swift -enum NetworkError: Error, CaseIterable { - case noConnectionToServer - case parsingFailed - - var localizedDescription: String { - switch self { - case .noConnectionToServer: "No connection to the server." - case .parsingFailed: "Data parsing failed." - } - } -} -``` - -You might expect this to work seamlessly, but trying it out reveals a surprise: 😱 - -```swift -struct ContentView: View { - var body: some View { - Button("Throw Random NetworkError") { - do { - throw NetworkError.allCases.randomElement()! - } catch { - print("Caught error with message: \(error.localizedDescription)") - } - } - } -} -``` - -The console output is not what you'd expect: - -```bash -Caught error with message: The operation couldn't be completed. (ErrorKitDemo.NetworkError error 0.) -``` - -There's no information about the specific error case - not even the enum case name appears, let alone your custom message! This happens because Swift's `Error` protocol is bridged to `NSError`, which uses a different system of `domain`, `code`, and `userInfo`. - -### The "Correct" Way: `LocalizedError` - -Swift provides `LocalizedError` as the "proper" solution, with these optional properties: -- `errorDescription: String?` -- `failureReason: String?` -- `recoverySuggestion: String?` -- `helpAnchor: String?` - -However, this approach has serious issues: -- All properties are optional - no compiler enforcement -- Only `errorDescription` affects `localizedDescription` -- `failureReason` and `recoverySuggestion` are often ignored -- `helpAnchor` is rarely used in modern development - -This makes `LocalizedError` both confusing and error-prone. - -## The Throwable Protocol Solution +### The Throwable Protocol -ErrorKit introduces the `Throwable` protocol to solve these issues: - -```swift -public protocol Throwable: LocalizedError { - var userFriendlyMessage: String { get } -} -``` - -This protocol is simple and clear: -- Named to align with Swift's `throw` keyword -- Follows Swift's naming convention (`able` suffix like `Codable`) -- Requires single, non-optional `userFriendlyMessage` property -- Guarantees your errors behave as expected - -Here's how you use it: +`Throwable` fixes the confusion of Swift's `Error` protocol by providing a clear, Swift-native approach to error handling: ```swift enum NetworkError: Throwable { @@ -104,11 +33,12 @@ enum NetworkError: Throwable { } ``` -When you print `error.localizedDescription`, you'll get exactly the message you expect! 🥳 - -### Quick Start During Development +Now when catching this error, you'll see exactly what you expect: +``` +"Unable to connect to the server." +``` -During early development phases when you're rapidly prototyping, `Throwable` allows you to define error messages using raw values for maximum speed: +For rapid development, you can use string raw values: ```swift enum NetworkError: String, Throwable { @@ -117,228 +47,33 @@ enum NetworkError: String, Throwable { } ``` -This approach eliminates boilerplate code while keeping error definitions concise and descriptive. However, remember to transition to proper localization using `String(localized:)` before shipping your app. - -### Summary - -> 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 `userFriendlyMessage(for:)` - -ErrorKit enhances error clarity through the `ErrorKit.userFriendlyMessage(for:)` function, designed to provide improved error descriptions for any error type. +[Read more about Throwable →](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/throwable-protocol) -### How It Works +### Enhanced Error Descriptions -The `userFriendlyMessage(for:)` function analyzes the provided `Error` and returns an enhanced message that's clear and helpful. It leverages a community-maintained collection of descriptions to ensure messages are accurate and continuously improving. - -### Supported Error Domains - -ErrorKit provides enhanced messages for errors from various domains: -- Foundation -- CoreData -- MapKit -- And many more... - -These domains are continuously updated to provide coverage for the most common error types in Swift development. - -### Usage Example - -Here's how to use `userFriendlyMessage(for:)` to handle errors gracefully: +Get improved, user-friendly messages for ANY error, including system errors: ```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 + // Better than localizedDescription, works with any error type print(ErrorKit.userFriendlyMessage(for: error)) - // Example output: "You are not connected to the Internet. Please check your connection." -} -``` - -### Why Use `userFriendlyMessage(for:)`? - -- **Clarity**: Returns clear and concise error messages, avoiding cryptic system-generated descriptions -- **Consistency**: Provides standardized error messaging across your application -- **Community-Driven**: Messages are regularly improved through developer contributions -- **Comprehensive**: Covers a wide range of common Swift error scenarios - -### 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. - -## 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. - -### Discovery and Usage - -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 instead 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. - -## Built-in Error Types for Common Scenarios - -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. - -### Why Built-in Types? - -Built-in error types offer several advantages: -- **Quick Start**: Begin with well-structured error handling without defining custom types -- **Consistency**: Use standardized error cases and messages across your codebase -- **Flexibility**: Easily transition to custom error types when you need more specific cases -- **Discoverability**: Clear naming conventions make it easy to find the right error type -- **Localization**: All error messages are pre-localized and user-friendly -- **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. - -### Available Error Types - -ErrorKit includes the following built-in error types: - -- **DatabaseError** (connectionFailed, operationFailed, recordNotFound) -- **FileError** (fileNotFound, readFailed, writeFailed) -- **NetworkError** (noInternet, timeout, badRequest, serverError, decodingFailure) -- **OperationError** (dependencyFailed, canceled, unknownFailure) -- **ParsingError** (invalidInput, missingField, inputTooLong) -- **PermissionError** (denied, restricted, notDetermined) -- **StateError** (invalidState, alreadyFinalized, preconditionFailed) -- **ValidationError** (invalidInput, missingField, inputTooLong) -- **GenericError** (for ad-hoc custom messages) - -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. - -### Usage Examples - -```swift -func fetchUserData() throws(DatabaseError) { - guard isConnected else { - throw .connectionFailed - } - // Fetching logic -} - -// Or with classical throws -func processData() throws { - guard isValid else { - throw ValidationError.invalidInput(field: "email") - } - // Processing logic -} - -// Quick error throwing with GenericError -func quickOperation() throws { - guard condition else { - throw GenericError(userFriendlyMessage: String(localized: "The condition X was not fulfilled, please check again.")) - } - // Operation logic -} - -// Using generic case for edge cases -func handleSpecialCase() throws(DatabaseError) { - guard specialCondition else { - throw .generic(userFriendlyMessage: String(localized: "Database is in maintenance mode")) - } - // Special case handling + // "You are not connected to the Internet. Please check your connection." } ``` -### Contributing New Error Types - -We need your help! If you find yourself: -- Defining similar error types across projects -- Missing a common error scenario in our built-in types -- Seeing patterns in error handling that could benefit others -- Having ideas for better error messages or new cases - -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. - -When contributing: -- Ensure error cases are generic enough for broad use -- Provide clear, actionable error messages -- Include real-world usage examples in documentation -- Follow the existing naming conventions +These enhanced descriptions are community-provided and fully localized mappings of common system errors to clearer, more actionable messages. -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. +[Read more about Enhanced Error Descriptions →](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/enhanced-error-descriptions) -## Simplified Error Nesting with the `Catching` Protocol +## Swift 6 Typed Throws Support -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. +Swift 6 introduces typed throws (`throws(ErrorType)`), bringing compile-time type checking to error handling. ErrorKit makes this powerful feature practical with solutions for its biggest challenges: -### The Problem with Manual Error Wrapping +### Error Nesting with Catching -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) - } -``` - -### 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: +The `Catching` protocol solves the biggest problem with error handling: nested errors. ```swift enum ProfileError: Throwable, Catching { @@ -349,7 +84,7 @@ enum ProfileError: Throwable, Catching { } struct ProfileRepository { - func loadProfile(id: String) throws(ProfileError) { + func loadProfile(id: String) throws(ProfileError) -> UserProfile { // Regular error throwing for validation guard id.isValidFormat else { throw ProfileError.validationFailed(field: "id") @@ -361,121 +96,23 @@ struct ProfileRepository { let settings = try fileSystem.readUserSettings(user.settingsPath) return UserProfile(user: user, settings: settings) } + + return userData } } ``` -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. - -## Enhanced Error Debugging with Error Chain Description +### Error Chain Debugging -One of the most challenging aspects of error handling in Swift is tracing where exactly an error originated, especially when using error wrapping across multiple layers of an application. ErrorKit solves this with powerful debugging tools that help you understand the complete error chain. - -### The Problem with Traditional Error Logging - -When logging errors in Swift, you typically lose context about how an error propagated through your application: - -```swift -} catch { - // 😕 Only shows the leaf error with no chain information - Logger().error("Error occurred: \(error)") - - // 😕 Shows a better message but still no error chain - Logger().error("Error: \(ErrorKit.userFriendlyMessage(for: error))") - // Output: "Could not find database file." -} -``` - -This makes it difficult to: -- Understand which module or layer originally threw the error -- Trace the error's path through your application -- Group similar errors for analysis -- Prioritize which errors to fix first - -### Solution: Error Chain Description - -ErrorKit's `errorChainDescription(for:)` function provides a comprehensive view of the entire error chain, showing you exactly how an error propagated through your application: +When using `Throwable` with the `Catching` protocol, you get powerful error chain debugging: ```swift do { try await updateUserProfile() } catch { - // 🎯 Always use this for debug logging Logger().error("\(ErrorKit.errorChainDescription(for: error))") - // Output shows the complete chain: + // Output shows the complete error path: // ProfileError // └─ DatabaseError // └─ FileError.notFound(path: "/Users/data.db") @@ -483,184 +120,200 @@ do { } ``` -This hierarchical view tells you: -1. Where the error originated (FileError) -2. How it was wrapped (DatabaseError → ProfileError) -3. What exactly went wrong (file not found) -4. The user-friendly message (reported to users) - -For errors conforming to the `Catching` protocol, you get the complete error wrapping chain. This is why it's important for your own error types and any Swift packages you develop to adopt both `Throwable` and `Catching` - it not only makes them work better with typed throws but also enables automatic extraction of the full error chain. +[Read more about Typed Throws and Error Nesting →](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/typed-throws-and-error-nesting) -Even for errors that don't conform to `Catching`, you still get valuable information since most Swift errors are enums. The error chain description will show you the exact enum case (e.g., `FileError.notFound`), making it easy to search your codebase for the error's origin. This is much better than the default cryptic message you get for enum cases when using `localizedDescription`. +## Ready-to-Use Tools -### Error Analytics with Grouping IDs +### Built-in Error Types -To help prioritize which errors to fix, ErrorKit provides `groupingID(for:)` that generates stable identifiers for errors sharing the exact same type structure and enum cases: +Stop reinventing common error types in every project. ErrorKit provides standardized error types for common scenarios: ```swift -struct ErrorTracker { - static func log(_ error: Error) { - // Get a stable ID that ignores dynamic parameters - let groupID = ErrorKit.groupingID(for: error) // e.g. "3f9d2a" - - Analytics.track( - event: "error_occurred", - properties: [ - "error_group": groupID, - "error_details": ErrorKit.errorChainDescription(for: error) - ] - ) +func fetchUserData() throws(DatabaseError) { + guard isConnected else { + throw .connectionFailed } + // Fetching logic } ``` -The grouping ID generates the same identifier for errors that have identical: -- Error type hierarchy -- Enum cases in the chain +Includes ready-to-use types like `DatabaseError`, `NetworkError`, `FileError`, `ValidationError`, `PermissionError`, and more - all conforming to both `Throwable` and `Catching` with localized messages. -But it ignores: -- Dynamic parameters (file paths, field names, etc.) -- User-friendly messages (which might be localized or dynamic) +For quick one-off errors, use `GenericError`: -For example, these errors have the same grouping ID since they differ only in their dynamic path parameters: ```swift -// Both generate groupID: "3f9d2a" -ProfileError -└─ DatabaseError - └─ FileError.notFound(path: "/Users/john/data.db") - └─ userFriendlyMessage: "Could not find database file." - -ProfileError -└─ DatabaseError - └─ FileError.notFound(path: "/Users/jane/backup.db") - └─ userFriendlyMessage: "Die Backup-Datenbank konnte nicht gefunden werden." -``` - -This precise grouping allows you to: -- Track true error frequencies in analytics without noise from dynamic data -- Create meaningful charts of most common error patterns -- Make data-driven decisions about which errors to fix first -- Monitor error trends over time - -### Summary - -ErrorKit's debugging tools transform error handling from a black box into a transparent system. By combining `errorChainDescription` for debugging with `groupingID` for analytics, you get deep insight into error flows while maintaining the ability to track and prioritize issues effectively. This is particularly powerful when combined with ErrorKit's `Catching` protocol, creating a comprehensive system for error handling, debugging, and monitoring. - - -## User Feedback with Error Logs - -When users encounter issues in your app, getting enough context to diagnose the problem can be challenging. Users rarely know what information you need, and reproducing issues without logs is often impossible. 😕 - -ErrorKit makes it simple to add diagnostic log collection to your app, providing crucial context for bug reports and support requests. - -### The Power of System Logs - -ErrorKit leverages Apple's unified logging system (`OSLog`/`Logger`) to collect valuable diagnostic information. If you're not already using structured logging, here's a quick primer: - -```swift -import OSLog - -// Log at appropriate levels -Logger().debug("Detailed connection info: \(details)") // Development debugging -Logger().info("User tapped on \(button)") // General information -Logger().notice("Successfully loaded user profile") // Important events -Logger().error("Failed to parse server response") // Errors that should be fixed -Logger().fault("Database corruption detected") // Critical system failures -``` - -ErrorKit can collect these logs based on level, giving you control over how much detail to include in reports. 3rd-party frameworks that also use Apple's unified logging system will be included so you get a full picture of what happened in your app, not just what you logged yourself. - -### Creating a Feedback Button with Automatic Log Collection - -The easiest way to implement a support system is using the `.mailComposer` SwiftUI modifier combined with `logAttachment`: - -```swift -struct ContentView: View { - @State private var showMailComposer = false - - var body: some View { - Form { - // Your app content here - - Button("Report a Problem") { - showMailComposer = true - } - .mailComposer( - isPresented: $showMailComposer, - recipient: "support@yourapp.com", - subject: " Bug Report", - messageBody: """ - Please describe what happened: - - - - ---------------------------------- - [Please do not remove the information below] - - App version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown") - Build: \(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown") - Device: \(UIDevice.current.model) - iOS: \(UIDevice.current.systemVersion) - """, - attachments: [ - try? ErrorKit.logAttachment(ofLast: .minutes(30), minLevel: .notice) - ] - ) - } +func quickOperation() throws { + guard condition else { + throw GenericError(userFriendlyMessage: "The operation couldn't be completed due to invalid state.") } + // Operation logic } ``` -This creates a simple "Report a Problem" button that: -1. Opens a pre-filled email composer -2. Includes useful device and app information -3. Automatically attaches recent system logs -4. Provides space for the user to describe the issue - -The above is just an example, feel free to adjust it to your needs and include any additional info needed. - -### Alternative Methods for More Control - -If you need more control over log handling, ErrorKit offers two additional approaches: +[Read more about Built-in Error Types →](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/built-in-error-types) -#### 1. Getting Log Data Directly +### User Feedback with Error Logs -For sending logs to your own backend or processing them in-app: +Gathering diagnostic information from users has never been simpler: ```swift -let logData = try ErrorKit.loggedData( - ofLast: .minutes(10), - minLevel: .notice -) - -// Use the data with your custom reporting system -analyticsService.sendLogs(data: logData) -``` - -#### 2. Exporting to a Temporary File - -For sharing logs via other mechanisms: - -```swift -let logFileURL = try ErrorKit.exportLogFile( - ofLast: .hours(1), - minLevel: .error -) - -// Share the log file -let activityVC = UIActivityViewController( - activityItems: [logFileURL], - applicationActivities: nil +Button("Report a Problem") { + showMailComposer = true +} +.mailComposer( + isPresented: $showMailComposer, + recipient: "support@yourapp.com", + subject: "Bug Report", + messageBody: "Please describe what happened:", + attachments: [ + try? ErrorKit.logAttachment(ofLast: .minutes(30)) + ] ) -present(activityVC, animated: true) ``` -### Benefits of Automatic Log Collection - -- **Better bug reports**: Get the context you need without asking users for technical details -- **Faster issue resolution**: See exactly what happened leading up to the problem -- **Lower support burden**: Reduce back-and-forth communications with users -- **User satisfaction**: Demonstrate that you take their problems seriously -- **Developer sanity**: Stop trying to reproduce issues with insufficient information - -By implementing a feedback button with automatic log collection, you transform the error reporting experience for both users and developers. Users can report issues with a single tap, and you get the diagnostic information you need to fix problems quickly. +With just a simple SwiftUI modifier, you can automatically include all log messages from Apple's unified logging system. + +[Read more about User Feedback and Logging →](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit/user-feedback-with-logs) + +## How These Features Work Together + +ErrorKit's features are designed to complement each other while remaining independently useful: + +1. **Start with improved error definitions** using `Throwable` for custom errors and `userFriendlyMessage(for:)` for system errors. + +2. **Add type safety with Swift 6 typed throws**, using the `Catching` protocol to solve nested error challenges. This pairs with error chain debugging to understand error flows through your app. + +3. **Save time with ready-made tools**: built-in error types for common scenarios and simple log collection for user feedback. + +## Adoption Path + +Here's a practical adoption strategy: + +1. Replace `Error` with `Throwable` in your custom error types +2. Use `ErrorKit.userFriendlyMessage(for:)` when showing system errors +3. Adopt built-in error types where they fit your needs +4. Implement typed throws with `Catching` for more robust error flows +5. Add error chain debugging to improve error visibility +6. Integrate log collection with your feedback system + +## Documentation + +For complete documentation visit: +[ErrorKit Documentation](https://swiftpackageindex.com/FlineDev/ErrorKit/documentation/errorkit) + +## Showcase + +I created this library for my own Indie apps (download & rate them to thank me!): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
App IconApp Name & DescriptionSupported Platforms
+ + + + + + TranslateKit: App Localizer + +
+ Indie-focused app localization with unmatched accuracy. Fast & easy: AI & proofreading, 125+ languages, market insights. Budget-friendly, free to try. +
Mac
+ + + + + + FreemiumKit: In-App Purchases + +
+ Simple In-App Purchases and Subscriptions for Apple Platforms: Automation, Paywalls, A/B Testing, Live Notifications, PPP, and more. +
iPhone, iPad, Mac, Vision
+ + + + + + Pleydia Organizer: Movie & Series Renamer + +
+ Simple, fast, and smart media management for your Movie, TV Show and Anime collection. +
Mac
+ + + + + + FreelanceKit: Time Tracking + +
+ Simple & affordable time tracking with a native experience for all  devices. iCloud sync & CSV export included. +
iPhone, iPad, Mac, Vision
+ + + + + + CrossCraft: Custom Crosswords + +
+ Create themed & personalized crosswords. Solve them yourself or share them to challenge others. +
iPhone, iPad, Mac, Vision
+ + + + + + FocusBeats: Pomodoro + Music + +
+ Deep Focus with proven Pomodoro method & select Apple Music playlists & themes. Automatically pauses music during breaks. +
iPhone, iPad, Mac, Vision
+ + + + + + Posters: Discover Movies at Home + +
+ Auto-updating & interactive posters for your home with trailers, showtimes, and links to streaming services. +
Vision
diff --git a/Sources/ErrorKit/ErrorKit.docc/ErrorKit.md b/Sources/ErrorKit/ErrorKit.docc/ErrorKit.md new file mode 100644 index 0000000..bcc3d0d --- /dev/null +++ b/Sources/ErrorKit/ErrorKit.docc/ErrorKit.md @@ -0,0 +1,68 @@ +# ``ErrorKit`` + +Making error handling in Swift more intuitive and powerful with clearer messages, type safety, and user-friendly diagnostics. + +@Metadata { + @PageImage(purpose: icon, source: "ErrorKit") +} + +## Overview + +Swift's error handling has several limitations that make it challenging to create robust, user-friendly applications: +- The `Error` protocol's confusing behavior with `localizedDescription` +- Hard-to-understand system error messages +- Limited type safety in error propagation +- Difficulties with error chain debugging +- Challenges in collecting meaningful feedback from users + +ErrorKit addresses these challenges with a suite of lightweight, interconnected features you can adopt progressively. + +## Core Features + +These foundational features improve how you define and present errors: + +@Links(visualStyle: detailedGrid) { + - + - +} + +## Swift 6 Typed Throws Support + +Swift 6 introduces typed throws (`throws(ErrorType)`), bringing compile-time type checking to error handling. ErrorKit makes this powerful feature practical with solutions for its biggest challenges: + +@Links(visualStyle: detailedGrid) { + - + - +} + +## Ready-to-Use Tools + +These practical tools help you implement robust error handling with minimal effort: + +@Links(visualStyle: detailedGrid) { + - + - +} + +## How These Features Work Together + +ErrorKit's features are designed to complement each other while remaining independently useful: + +1. **Start with improved error definitions** using `Throwable` for custom errors and `userFriendlyMessage(for:)` for system errors. + +2. **Add type safety with Swift 6 typed throws**, using the `Catching` protocol to solve nested error challenges. This pairs with error chain debugging to understand error flows through your app. + +3. **Save time with ready-made tools**: built-in error types for common scenarios and simple log collection for user feedback. + +Each feature builds upon the foundations laid by the previous ones, but you can adopt any part independently based on your needs. + +## Adoption Path + +Here's a practical adoption strategy: + +1. Replace `Error` with `Throwable` in your custom error types +2. Use `ErrorKit.userFriendlyMessage(for:)` when showing system errors +3. Adopt built-in error types where they fit your needs +4. Implement typed throws with `Catching` for more robust error flows +5. Add error chain debugging to improve error visibility +6. Integrate log collection with your feedback system diff --git a/Sources/ErrorKit/ErrorKit.docc/Guides/Built-in-Error-Types.md b/Sources/ErrorKit/ErrorKit.docc/Guides/Built-in-Error-Types.md new file mode 100644 index 0000000..72ef89d --- /dev/null +++ b/Sources/ErrorKit/ErrorKit.docc/Guides/Built-in-Error-Types.md @@ -0,0 +1,407 @@ +# Built-in Error Types + +Stop reinventing common error types with ready-to-use standardized errors. + +@Metadata { + @PageImage(purpose: icon, source: "ErrorKit") + @PageImage(purpose: card, source: "BuiltInErrorTypes") +} + +## Highlights + +ErrorKit provides pre-defined error types for common scenarios, reducing boilerplate and providing consistent error handling patterns across your projects. + +### Why Built-in Types? + +Most applications deal with similar error categories – database issues, network problems, file access errors, and validation failures. Defining these types repeatedly leads to: +- Duplicate work implementing similar error cases +- Inconsistent error handling patterns +- Lack of standardized error messages + +ErrorKit's built-in types offer several advantages: +- **Quick Start** – Begin with well-structured error handling without defining custom types +- **Consistency** – Use standardized error cases and messages across your codebase +- **Flexibility** – Easily transition to custom error types when you need more specific cases +- **Discoverability** – Clear naming conventions make it easy to find the right error type +- **Localization** – All error messages are pre-localized and user-friendly + +### Available Error Types + +ErrorKit provides a comprehensive set of built-in error types, each designed to address common error scenarios in Swift applications: + +- **DatabaseError** – For database connection and query issues +- **FileError** – For file system operations +- **NetworkError** – For network requests and API communications +- **ValidationError** – For input validation +- **StateError** – For state management and transitions +- **OperationError** – For operation execution issues +- **PermissionError** – For permission-related concerns +- **ParsingError** – For data parsing problems +- **GenericError** – For quick custom error cases + +Each of these types conforms to both `Throwable` and `Catching` protocols, providing seamless integration with [typed throws](Typed-Throws-and-Error-Nesting) and the [error chain debugging](Error-Chain-Debugging) system. + +### Ecosystem Impact + +As more Swift packages adopt these standardized error types, a powerful network effect emerges: + +- **Cross-package Communication** – Libraries can throw standard error types that applications understand and can handle intelligently +- **Smart Error Handling** – Instead of just showing error messages, apps can provide specific UI or recovery actions for known error types +- **Unified Error Experience** – Users experience consistent error handling patterns across different features and modules +- **Reduced Learning Curve** – Developers can learn one set of error patterns that work across multiple packages +- **Better Testing** – Standardized error types mean standardized testing patterns + +This standardization creates a more cohesive error handling experience throughout the Swift ecosystem, similar to how `Codable` created a standard for data serialization. + +## Database Error Handling + +The `DatabaseError` type addresses common database operation failures. Here's how you might use it: + +```swift +func fetchUserData(userId: String) throws(DatabaseError) { + guard isConnected else { + throw DatabaseError.connectionFailed + } + + guard let user = database.findUser(id: userId) else { + throw DatabaseError.recordNotFound(entity: "User", identifier: userId) + } + + do { + return try processUserData(user) + } catch { + // Automatically wrap any other errors + throw DatabaseError.caught(error) + } +} +``` + +`DatabaseError` includes the following cases: +- `.connectionFailed` – Unable to connect to the database +- `.operationFailed(context:)` – Query execution failed +- `.recordNotFound(entity:identifier:)` – Requested record doesn't exist +- `.caught(Error)` – For wrapping other errors +- `.generic(userFriendlyMessage:)` – For custom one-off scenarios + +### Core Data Example + +For context, here's how you might handle Core Data errors without ErrorKit: + +```swift +func updateProfile(name: String, bio: String) { + do { + try context.performChanges { + user.name = name + user.bio = bio + try context.save() + } + } catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSValidationError { + // Complex error checking with domain and code + showAlert(message: "Invalid data provided.") + } catch { + showAlert(message: "Unknown error occurred.") + } +} +``` + +And here's the same function using ErrorKit: + +```swift +func updateProfile(name: String, bio: String) { + do { + try context.performChanges { + user.name = name + user.bio = bio + try context.save() + } + } catch { + // Simpler error handling with better messages + showAlert(message: ErrorKit.userFriendlyMessage(for: error)) + } +} +``` + +The advantage is clear: ErrorKit eliminates the need for complex error domain/code checking while providing more descriptive error messages to users. + +## Network Error Handling + +The `NetworkError` type addresses common networking issues. Here's an example: + +```swift +func fetchProfileData() async throws(NetworkError) { + guard isNetworkReachable else { + throw NetworkError.noInternet + } + + let (data, response) = try await URLSession.shared.data(from: profileURL) + + guard let httpResponse = response as? HTTPURLResponse else { + throw NetworkError.badRequest(code: 0, message: "Invalid response") + } + + guard httpResponse.statusCode == 200 else { + throw NetworkError.serverError( + code: httpResponse.statusCode, + message: String(data: data, encoding: .utf8) + ) + } + + guard let profile = try? JSONDecoder().decode(Profile.self, from: data) else { + throw NetworkError.decodingFailure + } + + return profile +} +``` + +`NetworkError` includes the following cases: +- `.noInternet` – No network connection available +- `.timeout` – Request took too long to complete +- `.badRequest(code:message:)` – Client-side HTTP errors (400-499) +- `.serverError(code:message:)` – Server-side HTTP errors (500-599) +- `.decodingFailure` – Error parsing the response data +- `.caught(Error)` – For wrapping other errors +- `.generic(userFriendlyMessage:)` – For custom one-off scenarios + +### Custom Error Handling + +NetworkError enables enhanced user experiences through targeted handling: + +```swift +func fetchData() async { + do { + data = try await fetchProfileData() + state = .loaded + } catch { + state = .error + + // Specific handling for different network errors + switch error { + case NetworkError.noInternet, NetworkError.timeout: + // Show offline mode with cached data + refresh button + showOfflineView(with: cachedData) + + case let networkError as URLSessionError where networkError == .unauthorized: + // Trigger re-authentication flow + showAuthenticationPrompt() + + case let NetworkError.serverError(code, _) where code >= 500: + // Show maintenance message for server errors + showServerMaintenanceMessage() + + default: + // Default error handling + showErrorAlert(message: ErrorKit.userFriendlyMessage(for: error)) + } + } +} +``` + +## File System Error Handling + +The `FileError` type addresses common file operations issues: + +```swift +func loadConfiguration() throws(FileError) { + let configFile = "config.json" + guard FileManager.default.fileExists(atPath: configPath) else { + throw FileError.fileNotFound(fileName: configFile) + } + + do { + let data = try Data(contentsOf: URL(fileURLWithPath: configPath)) + return try JSONDecoder().decode(Configuration.self, from: data) + } catch let error as DecodingError { + throw FileError.readFailed(fileName: configFile) + } catch { + throw FileError.caught(error) + } +} +``` + +`FileError` includes the following cases: +- `.fileNotFound(fileName:)` – File doesn't exist +- `.readFailed(fileName:)` – Error reading from file +- `.writeFailed(fileName:)` – Error writing to file +- `.caught(Error)` – For wrapping other errors +- `.generic(userFriendlyMessage:)` – For custom one-off scenarios + +## Validation Error Handling + +The `ValidationError` type simplifies input validation: + +```swift +func validateRegistration(username: String, email: String, password: String) throws(ValidationError) { + guard !username.isEmpty else { + throw ValidationError.missingField(field: "Username") + } + + guard username.count <= 30 else { + throw ValidationError.inputTooLong(field: "Username", maxLength: 30) + } + + guard isValidEmail(email) else { + throw ValidationError.invalidInput(field: "Email") + } + + return RegistrationData(username: username, email: email, password: password) +} +``` + +`ValidationError` includes the following cases: +- `.invalidInput(field:)` – Input doesn't meet format requirements +- `.missingField(field:)` – Required field is empty +- `.inputTooLong(field:maxLength:)` – Input exceeds maximum length +- `.caught(Error)` – For wrapping other errors +- `.generic(userFriendlyMessage:)` – For custom one-off scenarios + +## Additional Error Types + +### Permission Error Handling + +The `PermissionError` type addresses authorization issues: + +```swift +func requestLocationAccess() throws(PermissionError) { + switch locationManager.authorizationStatus { + case .notDetermined: + throw PermissionError.notDetermined(permission: "Location") + case .restricted: + throw PermissionError.restricted(permission: "Location") + case .denied: + throw PermissionError.denied(permission: "Location") + case .authorizedAlways, .authorizedWhenInUse: + return // Permission granted + @unknown default: + throw PermissionError.generic(userFriendlyMessage: "Unknown location permission status") + } +} +``` + +### State Error Handling + +The `StateError` type manages invalid state transitions: + +```swift +func finalizeOrder(_ order: Order) throws(StateError) { + guard order.status == .verified else { + throw StateError.invalidState(description: "Order must be verified") + } + + guard !order.isFinalized else { + throw StateError.alreadyFinalized + } + + guard order.items.count > 0 else { + throw StateError.preconditionFailed(description: "Order must have at least one item") + } + + // Finalize order +} +``` + +### Operation Error Handling + +The `OperationError` type handles execution failures: + +```swift +func executeOperation() async throws(OperationError) { + guard !Task.isCancelled else { + throw OperationError.canceled + } + + guard dependenciesSatisfied() else { + throw OperationError.dependencyFailed(dependency: "Data Initialization") + } + + // Execute operation +} +``` + +### Parsing Error Handling + +The `ParsingError` type addresses data parsing issues: + +```swift +func parseUserData(from json: Data) throws(ParsingError) { + guard !json.isEmpty else { + throw ParsingError.invalidInput(input: "Empty JSON data") + } + + do { + let decoder = JSONDecoder() + return try decoder.decode(UserData.self, from: json) + } catch { + throw ParsingError.caught(error) + } +} +``` + +## Generic Error Handling + +For one-off errors without defining custom types, use `GenericError`: + +```swift +func quickOperation() throws { + guard condition else { + throw GenericError(userFriendlyMessage: "The operation couldn't be completed due to invalid state.") + } + + try GenericError.catch { + try riskyOperation() + } +} +``` + +`GenericError` is perfect during rapid development or for one-off error cases that don't justify creating a full custom type. You can always replace it with a more specific error type later when needed. + +## Flexible Error Handling with Generic Cases + +All built-in error types include a `.generic(userFriendlyMessage:)` case for edge cases: + +```swift +func handleSpecialCase() throws(DatabaseError) { + guard specialCondition else { + throw DatabaseError.generic(userFriendlyMessage: "Database is in maintenance mode until 10 PM.") + } + // Special case handling +} +``` + +This allows you to use the specific error type for categorization while providing a custom message for unusual situations. + +## Contributing New Error Types + +If you find yourself: +- Defining similar error types across projects +- Missing a common error scenario in our built-in types +- Seeing patterns in error handling that could benefit others +- Having ideas for better error messages or new cases + +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. + +When contributing: +- Ensure error cases are generic enough for broad use +- Provide clear, actionable error messages +- Include real-world usage examples in documentation +- 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. + +## Topics + +### Essentials + +- ``DatabaseError`` +- ``FileError`` +- ``GenericError`` +- ``NetworkError`` +- ``OperationError`` +- ``ParsingError`` +- ``PermissionError`` +- ``StateError`` +- ``ValidationError`` + +### Continue Reading + +- diff --git a/Sources/ErrorKit/ErrorKit.docc/Guides/Enhanced-Error-Descriptions.md b/Sources/ErrorKit/ErrorKit.docc/Guides/Enhanced-Error-Descriptions.md new file mode 100644 index 0000000..cf0cf76 --- /dev/null +++ b/Sources/ErrorKit/ErrorKit.docc/Guides/Enhanced-Error-Descriptions.md @@ -0,0 +1,127 @@ +# Enhanced Error Descriptions + +Transform cryptic system errors into clear, actionable messages with better descriptions. + +@Metadata { + @PageImage(purpose: icon, source: "ErrorKit") + @PageImage(purpose: card, source: "EnhancedDescriptions") +} + +## Highlights + +System errors from Apple frameworks often return messages that are too technical or vague to be helpful. ErrorKit provides enhanced, user-friendly descriptions for these errors through the `userFriendlyMessage(for:)` function. + +### The Problem with System Error Messages + +When working with system APIs, errors often have unclear or technical messages that may confuse users or fail to provide actionable information about how to resolve the issue. + +### The Solution: Enhanced Error Descriptions + +ErrorKit's `userFriendlyMessage(for:)` function provides improved error messages: + +```swift +do { + let url = URL(string: "https://example.com")! + let _ = try Data(contentsOf: url) +} catch { + // Instead of the default error message, get a more user-friendly one + print(ErrorKit.userFriendlyMessage(for: error)) +} +``` + +This function works with any error type, including system errors and your own custom errors. It maintains all the benefits of your custom `Throwable` types while enhancing system errors with more helpful messages. + +### Error Domain Coverage + +ErrorKit provides enhanced messages for errors from several system frameworks and domains: + +#### Foundation Domain +- Network errors (URLError) + - Connection issues + - Timeouts + - Host not found +- File operations (CocoaError) + - File not found + - Permission issues + - Disk space errors +- System errors (POSIXError) + - Disk space + - Access permission + - File descriptor issues + +#### CoreData Domain +- Store save errors +- Validation errors +- Relationship errors +- Store compatibility issues +- Model validation errors + +#### MapKit Domain +- Server failures +- Throttling errors +- Placemark not found +- Direction finding failures + +### Works Seamlessly with Throwable + +The `userFriendlyMessage(for:)` function integrates perfectly with ErrorKit's `Throwable` protocol: + +```swift +do { + try riskyOperation() +} catch { + // Works with both custom Throwable errors and system errors + showAlert(message: ErrorKit.userFriendlyMessage(for: error)) +} +``` + +If the error already conforms to `Throwable`, its `userFriendlyMessage` is used. For system errors, ErrorKit provides an enhanced description from its built-in mappings. + +### Localization Support + +All enhanced error messages are fully localized using the `String.localized(key:defaultValue:)` pattern, ensuring users receive messages in their preferred language where available. + +### How It Works + +The `userFriendlyMessage(for:)` function follows this process to determine the best error message: + +1. If the error conforms to `Throwable`, it uses the error's own `userFriendlyMessage` +2. It tries domain-specific handlers to find available enhanced versions +3. If the error conforms to `LocalizedError`, it combines its localized properties +4. As a fallback, it formats the NSError domain and code along with the standard `localizedDescription` + +### Contributing New Descriptions + +You can help improve ErrorKit by contributing better error descriptions for common error types: + +1. Identify cryptic error messages from system frameworks +2. Implement domain-specific handlers or extend existing ones (see folder `EnhancedDescriptions`) +3. Use clear, actionable language that helps users understand what went wrong +4. Include localization support for all messages (no need to actually localize, we'll take care) + +Example contribution to handle a new error type: + +```swift +// In ErrorKit+Foundation.swift +case let jsonError as NSError where jsonError.domain == NSCocoaErrorDomain && jsonError.code == 3840: + return String.localized( + key: "EnhancedDescriptions.JSONError.invalidFormat", + defaultValue: "The data couldn't be read because it isn't in the correct format." + ) +``` + +## Topics + +### Essentials + +- ``ErrorKit/userFriendlyMessage(for:)`` + +### Domain-Specific Handlers + +- ``ErrorKit/userFriendlyFoundationMessage(for:)`` +- ``ErrorKit/userFriendlyCoreDataMessage(for:)`` +- ``ErrorKit/userFriendlyMapKitMessage(for:)`` + +### Continue Reading + +- diff --git a/Sources/ErrorKit/ErrorKit.docc/Guides/Error-Chain-Debugging.md b/Sources/ErrorKit/ErrorKit.docc/Guides/Error-Chain-Debugging.md new file mode 100644 index 0000000..905adac --- /dev/null +++ b/Sources/ErrorKit/ErrorKit.docc/Guides/Error-Chain-Debugging.md @@ -0,0 +1,166 @@ +# Error Chain Debugging + +Trace the complete path of errors through your application with rich hierarchical debugging. + +@Metadata { + @PageImage(purpose: icon, source: "ErrorKit") + @PageImage(purpose: card, source: "ErrorChainDebugging") +} + +## Highlights + +One of the most challenging aspects of error handling in Swift is understanding exactly where an error originated, especially when using error wrapping across multiple layers of an application. ErrorKit solves this with powerful debugging tools that help you understand the complete error chain. + +### The Problem with Traditional Error Logging + +When logging errors in Swift, you typically lose context about how an error propagated through your application: + +```swift +do { + try await updateUserProfile() +} catch { + // 😕 Only shows the leaf error with no chain information + Logger().error("Error occurred: \(error)") + + // 😕 Shows a better message but still no error chain + Logger().error("Error: \(ErrorKit.userFriendlyMessage(for: error))") + // Output: "Could not find database file." +} +``` + +This makes it difficult to: +- Understand which module or layer originally threw the error +- Trace the error's path through your application +- Group similar errors for analysis +- Prioritize which errors to fix first + +### Solution: Error Chain Description + +ErrorKit's `errorChainDescription(for:)` function provides a comprehensive view of the entire error chain, showing you exactly how an error propagated through your application: + +```swift +do { + try await updateUserProfile() +} catch { + // 🎯 Always use this for debug logging + Logger().error("\(ErrorKit.errorChainDescription(for: error))") + + // Output shows the complete chain: + // ProfileError + // └─ DatabaseError + // └─ FileError.notFound(path: "/Users/data.db") + // └─ userFriendlyMessage: "Could not find database file." +} +``` + +This hierarchical view tells you: +1. Where the error originated (FileError) +2. How it was wrapped (DatabaseError → ProfileError) +3. What exactly went wrong (file not found) +4. The user-friendly message (reported to users) + +For errors conforming to the `Catching` protocol, you get the complete error wrapping chain. This is why it's important for your own error types and any Swift packages you develop to adopt both `Throwable` and `Catching` - it not only makes them work better with typed throws but also enables automatic extraction of the full error chain. + +Even for errors that don't conform to `Catching`, you still get valuable information since most Swift errors are enums. The error chain description will show you the exact enum case (e.g., `FileError.notFound`), making it easy to search your codebase for the error's origin. This is much better than the default cryptic message you get for enum cases when using `localizedDescription`. + +### Error Analytics with Grouping IDs + +To help prioritize which errors to fix, ErrorKit provides `groupingID(for:)` that generates stable identifiers for errors sharing the exact same type structure and enum cases: + +```swift +struct ErrorTracker { + static func log(_ error: Error) { + // Get a stable ID that ignores dynamic parameters + let groupID = ErrorKit.groupingID(for: error) // e.g. "3f9d2a" + + Analytics.track( + event: "error_occurred", + properties: [ + "error_group": groupID, + "error_details": ErrorKit.errorChainDescription(for: error) + ] + ) + } +} +``` + +The grouping ID generates the same identifier for errors that have identical: +- Error type hierarchy +- Enum cases in the chain + +But it ignores: +- Dynamic parameters (file paths, field names, etc.) +- User-friendly messages (which might be localized or dynamic) + +For example, these errors have the same grouping ID since they differ only in their dynamic path parameters: +```swift +// Both generate groupID: "3f9d2a" +ProfileError +└─ DatabaseError + └─ FileError.notFound(path: "/Users/john/data.db") + └─ userFriendlyMessage: "Could not find database file." + +ProfileError +└─ DatabaseError + └─ FileError.notFound(path: "/Users/jane/backup.db") + └─ userFriendlyMessage: "Die Backup-Datenbank konnte nicht gefunden werden." +``` + +This precise grouping allows you to track true error frequencies in analytics, create meaningful charts of common error patterns, and prioritize which errors to fix first. + +### Implementation and Integration + +Under the hood, `errorChainDescription(for:)` uses Swift's reflection to examine error objects, recursively traversing the error chain and formatting everything in a hierarchical tree structure with the user-friendly message at each leaf node. + +To integrate with your logging system: + +```swift +extension Logger { + func logError(_ error: Error, file: String = #file, function: String = #function, line: Int = #line) { + let errorChain = ErrorKit.errorChainDescription(for: error) + self.error("\(errorChain, privacy: .public)", file: file, function: function, line: line) + } +} +``` + +For crash reporting and analytics, include both the error chain and grouping ID: + +```swift +func reportCrash(_ error: Error) { + CrashReporting.send( + error: error, + metadata: [ + "errorChain": ErrorKit.errorChainDescription(for: error), + "errorGroup": ErrorKit.groupingID(for: error) + ] + ) +} +``` + +### Best Practices + +To get the most out of error chain debugging: + +1. **Use `Catching` consistently**: Add `Catching` conformance to all your error types that might wrap other errors. +2. **Include error chain descriptions in logs**: Always use `errorChainDescription(for:)` when logging errors. +3. **Group errors for analytics**: Use `groupingID(for:)` to track error frequencies. + +### Summary + +ErrorKit's debugging tools transform error handling from a black box into a transparent system. By combining `errorChainDescription` for debugging with `groupingID` for analytics, you get deep insight into error flows while maintaining the ability to track and prioritize issues effectively. This is particularly powerful when combined with ErrorKit's `Catching` protocol, creating a comprehensive system for error handling, debugging, and monitoring. + +## Topics + +### Essentials + +- ``ErrorKit/errorChainDescription(for:)`` +- ``ErrorKit/groupingID(for:)`` + +### Related Concepts + +- ``Catching`` +- ``Throwable`` + +### Continue Reading + +- diff --git a/Sources/ErrorKit/ErrorKit.docc/Guides/Throwable-Protocol.md b/Sources/ErrorKit/ErrorKit.docc/Guides/Throwable-Protocol.md new file mode 100644 index 0000000..a1ef20d --- /dev/null +++ b/Sources/ErrorKit/ErrorKit.docc/Guides/Throwable-Protocol.md @@ -0,0 +1,181 @@ +# Throwable Protocol + +Making error messages work as expected in Swift with a more intuitive protocol. + +@Metadata { + @PageImage(purpose: icon, source: "ErrorKit") + @PageImage(purpose: card, source: "ThrowableProtocol") +} + +## Highlights + +Swift's built-in `Error` protocol has a confusing quirk: custom `localizedDescription` messages don't work as expected. ErrorKit solves this with the `Throwable` protocol, ensuring your error messages always appear as intended. + +### The Problem with Swift's Error Protocol + +When you create a custom error type in Swift with a `localizedDescription` property: + +```swift +enum NetworkError: Error { + case noConnectionToServer + case parsingFailed + + var localizedDescription: String { + switch self { + case .noConnectionToServer: "No connection to the server." + case .parsingFailed: "Data parsing failed." + } + } +} +``` + +You expect to see your custom message when catching the error: + +```swift +do { + throw NetworkError.noConnectionToServer +} catch { + print(error.localizedDescription) + // Expected: "No connection to the server." + // Actual: "The operation couldn't be completed. (MyApp.NetworkError error 0.)" +} +``` + +Your custom message never appears! This happens because Swift's `Error` protocol is bridged to `NSError` behind the scenes, which uses a completely different system for error messages with `domain`, `code`, and `userInfo` dictionaries. + +### The "Official" Solution: LocalizedError + +Swift does provide `LocalizedError` as the officially recommended solution for customizing error messages. However, this protocol has serious issues that make it confusing and error-prone: + +```swift +enum NetworkError: LocalizedError { + case noConnectionToServer + case parsingFailed + + var errorDescription: String? { // Optional String! + switch self { + case .noConnectionToServer: "No connection to the server." + case .parsingFailed: "Data parsing failed." + } + } + + // Other optional properties that are often ignored + var failureReason: String? { nil } + var recoverySuggestion: String? { nil } + var helpAnchor: String? { nil } +} +``` + +The problems with `LocalizedError` include: +- All properties are optional (`String?`) – no compiler enforcement +- Only `errorDescription` affects `localizedDescription` – the others are often ignored +- `failureReason` and `recoverySuggestion` are rarely used by Apple frameworks +- `helpAnchor` is an outdated concept rarely used in modern development +- You still need to use String(localized:) for proper localization + +This makes `LocalizedError` both confusing and error-prone, especially for developers new to Swift. + +### The Solution: Throwable Protocol + +ErrorKit introduces the `Throwable` protocol to solve these problems: + +```swift +public protocol Throwable: LocalizedError { + var userFriendlyMessage: String { get } +} +``` + +The `Throwable` protocol is designed to be: +- Named to align with Swift's `throw` keyword for intuitive discovery +- Following Swift's naming convention (`able` suffix like `Codable`) +- Requiring a single, non-optional `userFriendlyMessage` property +- Extending `LocalizedError` for compatibility with existing systems +- Simple and clear with just one requirement + +Here's how you use it: + +```swift +enum NetworkError: Throwable { + case noConnectionToServer + case parsingFailed + + var userFriendlyMessage: String { + switch self { + case .noConnectionToServer: + String(localized: "Unable to connect to the server.") + case .parsingFailed: + String(localized: "Data parsing failed.") + } + } +} +``` + +Now when you catch errors: + +```swift +do { + throw NetworkError.noConnectionToServer +} catch { + print(error.localizedDescription) + // Now correctly shows: "Unable to connect to the server." +} +``` + +The `Throwable` protocol handles all the mapping between your custom messages and Swift's error system through a default implementation of `LocalizedError.errorDescription` that returns your `userFriendlyMessage`. + +### Quick Start with Raw Values + +For rapid development and prototyping, `Throwable` automatically works with string raw values: + +```swift +enum NetworkError: String, Throwable { + case noConnectionToServer = "Unable to connect to the server." + case parsingFailed = "Data parsing failed." +} +``` + +This eliminates boilerplate by automatically using the raw string values as your error messages. It's perfect for quickly implementing error types during active development before adding proper localization later. + +### Complete Drop-in Replacement + +`Throwable` is designed as a complete drop-in replacement for `Error`: + +```swift +// Standard Swift error-handling works exactly the same +func validateUser(name: String) throws { + guard name.count >= 3 else { + throw ValidationError.tooShort + } +} + +// Works with all existing Swift error patterns +do { + try validateUser(name: "Jo") +} catch let error as ValidationError { + // Type-based catching works + handleValidationError(error) +} catch { + // General error catching works + handleGenericError(error) +} +``` + +Any type that conforms to `Throwable` automatically conforms to `Error`, so you can use it with all existing Swift error handling patterns with no changes to your architecture. + +## Topics + +### Essentials + +- ``Throwable`` + +### Default Implementations + +- ``Swift/RawRepresentable/userFriendlyMessage`` + +### Error Handling + +- ``ErrorKit/userFriendlyMessage(for:)`` + +### Continue Reading + +- diff --git a/Sources/ErrorKit/ErrorKit.docc/Guides/Typed-Throws-and-Error-Nesting.md b/Sources/ErrorKit/ErrorKit.docc/Guides/Typed-Throws-and-Error-Nesting.md new file mode 100644 index 0000000..4a7cff6 --- /dev/null +++ b/Sources/ErrorKit/ErrorKit.docc/Guides/Typed-Throws-and-Error-Nesting.md @@ -0,0 +1,277 @@ +# Typed Throws and Error Nesting + +Making Swift 6's typed throws practical with seamless error propagation across layers. + +@Metadata { + @PageImage(purpose: icon, source: "ErrorKit") + @PageImage(purpose: card, source: "TypedThrowsAndNesting") +} + +## Highlights + +Swift 6 introduces typed throws (`throws(ErrorType)`) for stronger type safety in error handling. ErrorKit makes this powerful feature practical by solving the challenge of error propagation across layers with the `Catching` protocol. + +### Understanding Typed Throws + +Typed throws let you declare exactly which error types a function can throw: + +```swift +func processFile() throws(FileError) { + // This function can only throw FileError +} +``` + +This enables compile-time verification of error handling: + +```swift +do { + try processFile() +} catch FileError.fileNotFound { + // Handle specific case +} catch FileError.readFailed { + // Handle another specific case +} +// No need for a catch-all since all possibilities are covered +``` + +### System Function Overloads + +ErrorKit provides typed-throws overloads for common system APIs. To streamline discovery, these overloads use the same API names prefixed with "throwable": + +```swift +// Standard system API +try fileManager.createDirectory(at: url) + +// ErrorKit typed overload - same name with "throwable" prefix +try fileManager.throwableCreateDirectory(at: url) +``` + +The overloaded versions: +- Return the same results as the original functions +- Throw specific error types with detailed information +- Provide better error messages for common failures + +Available overloads include: + +#### FileManager Operations +```swift +// Creating directories +try FileManager.default.throwableCreateDirectory(at: url) + +// Removing items +try FileManager.default.throwableRemoveItem(at: url) + +// Copying files +try FileManager.default.throwableCopyItem(at: sourceURL, to: destinationURL) + +// Moving files +try FileManager.default.throwableMoveItem(at: sourceURL, to: destinationURL) +``` + +#### URLSession Operations +```swift +// Data tasks +let (data, response) = try await URLSession.shared.throwableData(from: url) + +// Handling HTTP status codes +try URLSession.shared.handleHTTPStatusCode(statusCode, data: data) +``` + +These typed overloads provide a more granular approach to error handling, allowing for precise error handling and improved user experience. + +### Enhanced User Experience with Typed Throws + +Using typed throws allows developers to implement smarter error handling with specific responses to different error types: + +```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` + switch error { + case .timeout, .requestTimeout, .tooManyRequests: + // Automatically retry the request with a backoff strategy + retryWithExponentialBackoff() + + case .noNetwork: + // Show an SF Symbol indicating the user is offline plus a retry button + showOfflineView() + + case .unauthorized: + // Redirect the user to your login-flow (e.g. because token expired) + startAuthenticationFlow() + + default: + // Fall back to showing error message + showErrorAlert(message: error.localizedDescription) + } +} +``` + +This specific error handling enables you to: +- Implement automatic retry strategies for transient errors +- Show UI appropriate to the specific error condition +- Trigger authentication flows for permission issues +- Provide a better overall user experience than generic error handling + +### The Problem: Error Propagation + +While typed throws improves type safety, it creates a challenge when propagating errors through multiple layers of an application. Without ErrorKit, you'd need to manually wrap errors at each layer: + +```swift +enum ProfileError: Error { + case validationFailed(field: String) + case databaseError(DatabaseError) // Wrapper case needed for database errors + case networkError(NetworkError) // Another wrapper for network errors + + var errorDescription: String { /* ... */ } +} + +func loadProfile(id: String) throws(ProfileError) { + // Regular error throwing for validation + guard id.isValidFormat else { + throw ProfileError.validationFailed(field: "id") + } + + // Manually mapping nested errors + do { + let user = try database.loadUser(id) + do { + let settings = try fileSystem.readUserSettings(user.settingsPath) + return UserProfile(user: user, settings: settings) + } catch let error as NetworkError { + throw ProfileError.networkError(error) // Manual wrapping + } + } catch let error as DatabaseError { + throw ProfileError.databaseError(error) // Manual wrapping + } +} +``` + +This approach requires: +1. Creating explicit wrapper cases for each possible error type +2. Writing repetitive do-catch blocks for manual error conversion +3. Maintaining this wrapping code as your error types evolve + +### The Solution: Catching Protocol + +ErrorKit's `Catching` protocol provides a clean solution: + +```swift +enum ProfileError: Throwable, Catching { + case validationFailed(field: String) + case caught(Error) // Single case handles all nested errors + + var userFriendlyMessage: String { /* ... */ } +} + +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) + } + + return userData +} +``` + +The `catch` function automatically wraps any errors thrown in its closure into the `caught` case, preserving both type safety and the error's original information. + +### Best Practices for Using Catching + +To get the most out of the `Catching` protocol, follow these best practices: + +#### 1. When to Add Catching + +Add `Catching` conformance when: +- Your error type might need to wrap errors from lower-level modules +- You're using typed throws and calling functions that throw different error types +- You want to create a hierarchy of errors for better organization + +You'll know you need `Catching` when you see yourself writing error wrapper cases like: +```swift +enum MyError: Error { + case specificError + case otherModuleError(OtherError) // If you're writing wrapper cases, you need Catching +} +``` + +#### 2. Error Hierarchy Structure + +Keep your error hierarchies shallow when possible: +- Aim for 2-3 levels at most (e.g., AppError → ModuleError → SystemError) +- Use specific error cases for known errors, and `caught` for others +- Consider organizing by module or feature rather than error type + +#### 3. Preserve User-Friendly Messages + +When implementing `userFriendlyMessage` for a `Catching` type: + +```swift +var userFriendlyMessage: String { + switch self { + case .specificError: + return "A specific error occurred." + case .caught(let error): + // Use ErrorKit's enhanced messages for wrapped errors + return ErrorKit.userFriendlyMessage(for: error) + } +} +``` + +This ensures that user-friendly messages propagate correctly through the error chain. + +#### 4. Use with Built-in Error Types + +All of ErrorKit's built-in error types already conform to `Catching`, so you can easily wrap system errors: + +```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() + } +} +``` + +### Working with Error Chain Debugging + +For complete visibility into your error chains, ErrorKit provides powerful debugging tools that work perfectly with `Catching`. These tools are covered in detail in the [Error Chain Debugging](Error-Chain-Debugging) documentation. + +When using typed throws with the `Catching` protocol, you'll benefit greatly from these debugging capabilities, as they allow you to: + +- Visualize the complete error chain hierarchy +- Track errors through different application layers +- Identify patterns in error propagation +- Prioritize which errors to fix first + +Be sure to explore the error chain debugging features to get the full benefit of ErrorKit's typed throws support. + +## Topics + +### Essentials + +- ``Catching`` +- ``Catching/catch(_:)`` + +### System Overloads + +- ``FileManager/throwableCreateDirectory(at:withIntermediateDirectories:attributes:)`` +- ``FileManager/throwableRemoveItem(at:)`` +- ``URLSession/throwableData(for:)`` +- ``URLSession/throwableData(from:)`` +- ``URLSession/handleHTTPStatusCode(_:data:)`` + +### Continue Reading + +- diff --git a/Sources/ErrorKit/ErrorKit.docc/Guides/User-Feedback-with-Logs.md b/Sources/ErrorKit/ErrorKit.docc/Guides/User-Feedback-with-Logs.md new file mode 100644 index 0000000..5d78dd8 --- /dev/null +++ b/Sources/ErrorKit/ErrorKit.docc/Guides/User-Feedback-with-Logs.md @@ -0,0 +1,243 @@ +# User Feedback with Logs + +Simplify bug reports with automatic log collection from Apple's unified logging system. + +@Metadata { + @PageImage(purpose: icon, source: "ErrorKit") + @PageImage(purpose: card, source: "UserFeedbackWithLogs") +} + +## Highlights + +When users encounter issues in your app, getting enough context to diagnose the problem is crucial. ErrorKit makes it simple to add diagnostic log collection to your app, providing valuable context for bug reports and support requests. + +### The Challenge of User Feedback + +When users report problems, they often lack the technical knowledge to provide the necessary details: +- They don't know what information you need to diagnose the issue +- They can't easily access system logs or technical details +- They may struggle to reproduce complex issues on demand +- The steps they describe might be incomplete or unclear + +Without proper context, developers face significant challenges: +- Time wasted in back-and-forth communications asking for more information +- Difficulty reproducing issues that occur only on specific devices or configurations +- Inability to diagnose intermittent problems that happen infrequently +- Frustration for both users and developers as issues remain unresolved + +### The Power of System Logs + +ErrorKit leverages Apple's unified logging system (`OSLog`/`Logger`) to collect valuable diagnostic information. If you're not already using structured logging, here's a quick introduction: + +```swift +import OSLog + +// Create a logger - optionally with subsystem and category +let logger = Logger() +// or +let networkLogger = Logger(subsystem: "com.yourapp", category: "networking") + +// Log at appropriate levels +logger.trace("Very detailed tracing info") // Alias for debug +logger.debug("Detailed connection info") // Development debugging +logger.info("User tapped submit button") // General information +logger.notice("Profile successfully loaded") // Important events +logger.warning("Low disk space detected") // Alias for error +logger.error("Failed to load user data") // Errors that should be fixed +logger.critical("Payment processing failed") // Critical issues (alias for fault) +logger.fault("Database corruption detected") // System failures + +// Format values and control privacy +logger.info("User \(userId, privacy: .private) logged in from \(ipAddress, privacy: .public)") +logger.debug("Memory usage: \(bytes, format: .byteCount)") +``` + +Apple's logging system offers significant advantages over `print()` statements: +- Privacy controls for sensitive data +- Efficient performance with minimal overhead +- Log levels for filtering information +- System-wide integration +- Persistence across app launches +- Console integration for debugging + +### Comprehensive Log Collection + +A key advantage of ErrorKit's log collection is that it captures not just your app's logs, but also relevant logs from: + +1. **Third-party frameworks** that use Apple's unified logging system +2. **System components** your app interacts with (networking, file system, etc.) +3. **Background processes** related to your app's functionality + +This gives you a complete picture of what was happening in and around your app when the issue occurred, not just the logs you explicitly added. This comprehensive context is often crucial for diagnosing complex issues that involve multiple components. + +### Creating a Feedback Button + +The easiest way to implement error reporting is with the `.mailComposer` SwiftUI modifier: + +```swift +struct ContentView: View { + @State private var showMailComposer = false + + var body: some View { + Form { + // Your app content + + Button("Report a Problem") { + showMailComposer = true + } + .mailComposer( + isPresented: $showMailComposer, + recipient: "support@yourapp.com", + subject: "Bug Report", + messageBody: """ + Please describe what happened: + + + + ---------------------------------- + Device: \(UIDevice.current.model) + iOS: \(UIDevice.current.systemVersion) + App version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown") + """, + attachments: [ + try? ErrorKit.logAttachment(ofLast: .minutes(30)) + ] + ) + } + } +} +``` + +This creates a simple "Report a Problem" button that: +1. Opens a pre-filled email composer +2. Includes useful device and app information +3. Automatically attaches recent system logs +4. Provides space for the user to describe the issue + +### Controlling Log Collection + +ErrorKit offers several options for controlling log collection: + +```swift +// Collect logs from the last 30 minutes with notice level or higher +try ErrorKit.logAttachment(ofLast: .minutes(30), minLevel: .notice) + +// Collect logs from the last hour with error level or higher (fewer, more important logs) +try ErrorKit.logAttachment(ofLast: .hours(1), minLevel: .error) + +// Collect logs from the last 5 minutes with debug level or higher (very detailed) +try ErrorKit.logAttachment(ofLast: .minutes(5), minLevel: .debug) +``` + +The `minLevel` parameter lets you control how verbose the logs are: +- `.debug`: All logs (very verbose) +- `.info`: Informational logs and above +- `.notice`: Notable events (default) +- `.error`: Only errors and faults +- `.fault`: Only critical errors + +### Alternative Methods + +If you need more control over log handling, ErrorKit offers additional approaches: + +#### Getting Log Data Directly + +For sending logs to your own backend or processing them in-app: + +```swift +let logData = try ErrorKit.loggedData( + ofLast: .minutes(10), + minLevel: .notice +) + +// Use the data with your custom reporting system +analyticsService.sendLogs(data: logData) +``` + +#### Exporting to a Temporary File + +For sharing logs via other mechanisms: + +```swift +let logFileURL = try ErrorKit.exportLogFile( + ofLast: .hours(1), + minLevel: .error +) + +// Share the log file +let activityVC = UIActivityViewController( + activityItems: [logFileURL], + applicationActivities: nil +) +present(activityVC, animated: true) +``` + +### Transforming the Support Experience + +Implementing a feedback button with automatic log collection transforms the support experience for both users and developers: + +#### For Users: +- **Simplified Reporting**: Submit feedback with a single tap, no technical knowledge required +- **No Technical Questions**: Avoid frustrating back-and-forth asking for technical details +- **Faster Resolution**: Issues can be diagnosed and fixed more quickly +- **Better Experience**: Shows users you take their problems seriously with professional tools + +#### For Developers: +- **Complete Context**: See exactly what was happening when the issue occurred +- **Reduced Support Burden**: Less time spent asking for additional information +- **Better Reproduction**: More reliable reproduction steps based on log data +- **Efficient Debugging**: Quickly identify patterns in error reports +- **Developer Sanity**: Stop trying to reproduce issues with insufficient information + +The investment in proper log collection pays dividends in reduced support costs, faster issue resolution, and improved user satisfaction. + +### Best Practices for Logging + +To maximize the value of ErrorKit's log collection: + +1. **Use Apple's Logger Instead of Print**: + ```swift + // Instead of: + print("User logged in: \(username)") + + // Use: + Logger().info("User logged in: \(username, privacy: .private)") + ``` + +2. **Choose Appropriate Log Levels**: + - `.debug` for developer details that are only needed during development + - `.info` for general tracking of normal app flow + - `.notice` for important events users would care about + - `.error` for problems that need fixing but don't prevent core functionality + - `.fault` for critical issues that break core functionality + +3. **Include Context in Logs**: + ```swift + // Instead of: + Logger().error("Failed to load") + + // Use: + Logger().error("Failed to load document \(documentId): \(error.localizedDescription)") + ``` + +4. **Protect Sensitive Information**: + ```swift + Logger().info("Processing payment for user \(userId, privacy: .private)") + ``` + +By implementing these best practices along with ErrorKit's log collection, you create a robust system for gathering the context needed to diagnose and fix issues efficiently. + +## Topics + +### Essentials + +- ``ErrorKit/logAttachment(ofLast:minLevel:filename:)`` +- ``ErrorKit/loggedData(ofLast:minLevel:)`` +- ``ErrorKit/exportLogFile(ofLast:minLevel:)`` + +### Helper Types + +- ``MailAttachment`` +- ``Duration/timeInterval`` +- ``Duration/minutes(_:)`` +- ``Duration/hours(_:)`` diff --git a/Sources/ErrorKit/ErrorKit.docc/Resources/BuiltInErrorTypes.jpg b/Sources/ErrorKit/ErrorKit.docc/Resources/BuiltInErrorTypes.jpg new file mode 100644 index 0000000..2d2aefd Binary files /dev/null and b/Sources/ErrorKit/ErrorKit.docc/Resources/BuiltInErrorTypes.jpg differ diff --git a/Sources/ErrorKit/ErrorKit.docc/Resources/EnhancedDescriptions.jpg b/Sources/ErrorKit/ErrorKit.docc/Resources/EnhancedDescriptions.jpg new file mode 100644 index 0000000..b7989f0 Binary files /dev/null and b/Sources/ErrorKit/ErrorKit.docc/Resources/EnhancedDescriptions.jpg differ diff --git a/Sources/ErrorKit/ErrorKit.docc/Resources/ErrorChainDebugging.jpg b/Sources/ErrorKit/ErrorKit.docc/Resources/ErrorChainDebugging.jpg new file mode 100644 index 0000000..586a613 Binary files /dev/null and b/Sources/ErrorKit/ErrorKit.docc/Resources/ErrorChainDebugging.jpg differ diff --git a/Sources/ErrorKit/ErrorKit.docc/Resources/ThrowableProtocol.jpg b/Sources/ErrorKit/ErrorKit.docc/Resources/ThrowableProtocol.jpg new file mode 100644 index 0000000..f220b0d Binary files /dev/null and b/Sources/ErrorKit/ErrorKit.docc/Resources/ThrowableProtocol.jpg differ diff --git a/Sources/ErrorKit/ErrorKit.docc/Resources/TypedThrowsAndNesting.jpg b/Sources/ErrorKit/ErrorKit.docc/Resources/TypedThrowsAndNesting.jpg new file mode 100644 index 0000000..df64a02 Binary files /dev/null and b/Sources/ErrorKit/ErrorKit.docc/Resources/TypedThrowsAndNesting.jpg differ diff --git a/Sources/ErrorKit/ErrorKit.docc/Resources/UserFeedbackWithLogs.jpg b/Sources/ErrorKit/ErrorKit.docc/Resources/UserFeedbackWithLogs.jpg new file mode 100644 index 0000000..fa6e148 Binary files /dev/null and b/Sources/ErrorKit/ErrorKit.docc/Resources/UserFeedbackWithLogs.jpg differ diff --git a/Sources/ErrorKit/ErrorKit.docc/theme-settings.json b/Sources/ErrorKit/ErrorKit.docc/theme-settings.json new file mode 100644 index 0000000..8b3a69d --- /dev/null +++ b/Sources/ErrorKit/ErrorKit.docc/theme-settings.json @@ -0,0 +1,11 @@ +{ + "theme": { + "color": { + "header": "#082B4B", + "documentation-intro-title": "#FFFFFF", + "documentation-intro-figure": "#FFFFFF", + "documentation-intro-fill": "radial-gradient(circle at top, var(--color-header) 30%, #000 100%)", + "documentation-intro-accent": "var(--color-header)" + } + } +}