Skip to content

Commit 1b13790

Browse files
committed
Merge branch 'wip/string-interpolation'
# Conflicts: # Sources/ErrorKit/Helpers/String+ErrorKit.swift
2 parents 4dee83d + 197ad48 commit 1b13790

File tree

6 files changed

+124
-5
lines changed

6 files changed

+124
-5
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ do {
6767
// Better than localizedDescription, works with any error type
6868
print(ErrorKit.userFriendlyMessage(for: error))
6969
// "You are not connected to the Internet. Please check your connection."
70+
71+
// String interpolation automatically uses userFriendlyMessage(for:)
72+
print("Request failed: \(error)")
7073
}
7174
```
7275

Sources/ErrorKit/ErrorKit.docc/Guides/Enhanced-Error-Descriptions.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ If the error already conforms to `Throwable`, its `userFriendlyMessage` is used.
8181

8282
All enhanced error messages are fully localized using the `String(localized:)` pattern, ensuring users receive messages in their preferred language where available.
8383

84+
### String Interpolation Convenience
85+
86+
ErrorKit enhances Swift's string interpolation to automatically use `userFriendlyMessage(for:)`:
87+
88+
```swift
89+
// Instead of:
90+
showAlert(message: "Save failed: \(ErrorKit.userFriendlyMessage(for: error))")
91+
92+
// You can simply use:
93+
showAlert(message: "Save failed: \(error)")
94+
Text("Could not load data: \(error)")
95+
Logger().info("Sync completed with error: \(error)")
96+
```
97+
98+
This works with any error type — both your custom `Throwable` errors and system errors.
99+
84100
### How It Works
85101

86102
The `userFriendlyMessage(for:)` function follows this process to determine the best error message:

Sources/ErrorKit/ErrorKit.docc/Guides/Error-Chain-Debugging.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ For errors conforming to the `Catching` protocol, you get the complete error wra
6363

6464
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`.
6565

66+
### String Interpolation for Debug Logging
67+
68+
ErrorKit enhances string interpolation for error chain debugging. Use either `chain:` or `debug:` (they're aliases) to get the complete hierarchical description:
69+
70+
```swift
71+
// Instead of:
72+
Logger().error("Update failed: \(ErrorKit.errorChainDescription(for: error))")
73+
74+
// You can simply use either:
75+
Logger().error("Update failed:\n\(chain: error)")
76+
Logger().error("Update failed:\n\(debug: error)")
77+
```
78+
79+
Use `\(error)` for user-facing messages, `\(chain: error)` or `\(debug: error)` for debugging.
80+
6681
### Error Analytics with Grouping IDs
6782

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

Sources/ErrorKit/ErrorKit.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ public enum ErrorKit {
168168
} else {
169169
// For enums, include the full case description with type name
170170
if let enclosingType {
171-
return "\(enclosingType).\(error)"
171+
return "\(enclosingType).\(String(describing: error))"
172172
} else {
173173
return String(describing: error)
174174
}

Sources/ErrorKit/Helpers/String+ErrorKit.swift

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,57 @@ extension String {
1919
}
2020

2121
extension String.StringInterpolation {
22-
mutating public func appendInterpolation(error: some Error) {
23-
appendInterpolation(ErrorKit.userFriendlyMessage(for: error))
22+
/// Interpolates an error using its user-friendly message.
23+
///
24+
/// Uses ``ErrorKit.userFriendlyMessage(for:)`` to provide clear, actionable error descriptions
25+
/// suitable for displaying to users. For nested errors, returns the message from the root cause.
26+
///
27+
/// ```swift
28+
/// showAlert("Operation failed: \(error)")
29+
/// ```
30+
///
31+
/// - Parameter error: The error to interpolate using its user-friendly message
32+
mutating public func appendInterpolation(_ error: some Error) {
33+
self.appendInterpolation(ErrorKit.userFriendlyMessage(for: error))
2434
}
2535

26-
mutating public func appendInterpolation(errorChain error: some Error) {
27-
appendInterpolation(ErrorKit.errorChainDescription(for: error))
36+
/// Interpolates an error using its complete chain description for debugging.
37+
///
38+
/// Uses ``ErrorKit.errorChainDescription(for:)`` to show the full error hierarchy,
39+
/// type information, and nested structure. Ideal for logging and debugging.
40+
///
41+
/// ```swift
42+
/// Logger().error("Operation failed with:\n\(chain: error)")
43+
/// // Operation failed with:
44+
/// // DatabaseError
45+
/// // └─ FileError
46+
/// // └─ PermissionError.denied(permission: "~/Downloads/Profile.png")
47+
/// // └─ userFriendlyMessage: "Access to ~/Downloads/Profile.png was declined..."
48+
/// ```
49+
///
50+
/// - Parameter error: The error to interpolate using its complete chain description
51+
mutating public func appendInterpolation(chain error: some Error) {
52+
self.appendInterpolation(ErrorKit.errorChainDescription(for: error))
53+
}
54+
55+
/// Interpolates an error using its complete chain description for debugging.
56+
///
57+
/// Uses ``ErrorKit.errorChainDescription(for:)`` to show the full error hierarchy,
58+
/// type information, and nested structure. Ideal for logging and debugging.
59+
///
60+
/// ```swift
61+
/// Logger().error("Operation failed with:\n\(chain: error)")
62+
/// // Operation failed with:
63+
/// // DatabaseError
64+
/// // └─ FileError
65+
/// // └─ PermissionError.denied(permission: "~/Downloads/Profile.png")
66+
/// // └─ userFriendlyMessage: "Access to ~/Downloads/Profile.png was declined..."
67+
/// ```
68+
///
69+
/// - Parameter error: The error to interpolate using its complete chain description
70+
///
71+
/// - NOTE: Alias for ``appendInterpolation(chain:)``.
72+
mutating public func appendInterpolation(debug error: some Error) {
73+
self.appendInterpolation(chain: error)
2874
}
2975
}

Tests/ErrorKitTests/ErrorKitTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,45 @@ enum ErrorKitTests {
6161
}
6262
}
6363

64+
enum StringInterpolation {
65+
@Test
66+
static func implicitWithStruct() async throws {
67+
#expect("\(SomeThrowable())" == "Something failed hard.")
68+
}
69+
70+
@Test
71+
static func implicitWithNestedError() async throws {
72+
let nestedError = DatabaseError.caught(FileError.caught(PermissionError.denied(permission: "~/Downloads/Profile.png")))
73+
#expect("\(nestedError)" == "Access to ~/Downloads/Profile.png was declined. To use this feature, please enable the permission in your device Settings.")
74+
}
75+
76+
@Test
77+
static func chainWithStruct() async throws {
78+
#expect(
79+
"\(chain: SomeThrowable())"
80+
==
81+
"""
82+
SomeThrowable [Struct]
83+
└─ userFriendlyMessage: "Something failed hard."
84+
"""
85+
)
86+
}
87+
88+
@Test
89+
static func chainWithNestedError() async throws {
90+
let nestedError = DatabaseError.caught(FileError.caught(PermissionError.denied(permission: "~/Downloads/Profile.png")))
91+
#expect(
92+
"\(chain: nestedError)"
93+
==
94+
"""
95+
DatabaseError
96+
└─ FileError
97+
└─ PermissionError.denied(permission: "~/Downloads/Profile.png")
98+
└─ userFriendlyMessage: "Access to ~/Downloads/Profile.png was declined. To use this feature, please enable the permission in your device Settings."
99+
""")
100+
}
101+
}
102+
64103
enum ErrorChainDescription {
65104
@Test
66105
static func localizedError() {

0 commit comments

Comments
 (0)