@aicgen/aicgen 1.0.0-beta.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{.claude/guidelines → .agent/rules}/api-design.md +5 -1
- package/{.claude/guidelines → .agent/rules}/architecture.md +5 -1
- package/{.claude/guidelines → .agent/rules}/best-practices.md +5 -1
- package/{.claude/guidelines → .agent/rules}/code-style.md +5 -1
- package/{.claude/guidelines → .agent/rules}/design-patterns.md +5 -1
- package/{.claude/guidelines → .agent/rules}/devops.md +5 -1
- package/{.claude/guidelines → .agent/rules}/error-handling.md +5 -1
- package/.agent/rules/instructions.md +28 -0
- package/{.claude/guidelines → .agent/rules}/language.md +5 -1
- package/{.claude/guidelines → .agent/rules}/performance.md +5 -1
- package/{.claude/guidelines → .agent/rules}/security.md +5 -1
- package/{.claude/guidelines → .agent/rules}/testing.md +5 -1
- package/.agent/workflows/add-documentation.md +10 -0
- package/.agent/workflows/generate-integration-tests.md +10 -0
- package/.agent/workflows/generate-unit-tests.md +11 -0
- package/.agent/workflows/performance-audit.md +11 -0
- package/.agent/workflows/refactor-extract-module.md +12 -0
- package/.agent/workflows/security-audit.md +12 -0
- package/.gemini/instructions.md +4843 -0
- package/.vs/ProjectSettings.json +2 -2
- package/.vs/VSWorkspaceState.json +15 -15
- package/.vs/aicgen.slnx/v18/DocumentLayout.json +53 -53
- package/AGENTS.md +9 -11
- package/assets/icon.svg +33 -33
- package/bun.lock +734 -26
- package/{CLAUDE.md → claude.md} +2 -2
- package/config.example.yml +129 -0
- package/config.yml +38 -0
- package/data/architecture/microservices/api-gateway.md +56 -56
- package/data/devops/observability.md +73 -73
- package/data/guideline-mappings.yml +128 -0
- package/data/language/dart/async.md +289 -0
- package/data/language/dart/basics.md +280 -0
- package/data/language/dart/error-handling.md +355 -0
- package/data/language/dart/index.md +10 -0
- package/data/language/dart/testing.md +352 -0
- package/data/language/swift/basics.md +477 -0
- package/data/language/swift/concurrency.md +654 -0
- package/data/language/swift/error-handling.md +679 -0
- package/data/language/swift/swiftui-mvvm.md +795 -0
- package/data/language/swift/testing.md +708 -0
- package/data/version.json +10 -8
- package/dist/index.js +50153 -28959
- package/jest.config.js +46 -0
- package/package.json +14 -3
- package/.claude/agents/architecture-reviewer.md +0 -88
- package/.claude/agents/guideline-checker.md +0 -73
- package/.claude/agents/security-auditor.md +0 -108
- package/.claude/settings.json +0 -98
- package/.claude/settings.local.json +0 -8
- package/.eslintrc.json +0 -28
- package/.github/workflows/release.yml +0 -180
- package/.github/workflows/test.yml +0 -81
- package/CONTRIBUTING.md +0 -821
- package/dist/commands/init.d.ts +0 -8
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -46
- package/dist/commands/init.js.map +0 -1
- package/dist/config/profiles.d.ts +0 -4
- package/dist/config/profiles.d.ts.map +0 -1
- package/dist/config/profiles.js +0 -30
- package/dist/config/profiles.js.map +0 -1
- package/dist/config/settings.d.ts +0 -7
- package/dist/config/settings.d.ts.map +0 -1
- package/dist/config/settings.js +0 -7
- package/dist/config/settings.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/models/guideline.d.ts +0 -15
- package/dist/models/guideline.d.ts.map +0 -1
- package/dist/models/guideline.js +0 -2
- package/dist/models/guideline.js.map +0 -1
- package/dist/models/preference.d.ts +0 -9
- package/dist/models/preference.d.ts.map +0 -1
- package/dist/models/preference.js +0 -2
- package/dist/models/preference.js.map +0 -1
- package/dist/models/profile.d.ts +0 -9
- package/dist/models/profile.d.ts.map +0 -1
- package/dist/models/profile.js +0 -2
- package/dist/models/profile.js.map +0 -1
- package/dist/models/project.d.ts +0 -13
- package/dist/models/project.d.ts.map +0 -1
- package/dist/models/project.js +0 -2
- package/dist/models/project.js.map +0 -1
- package/dist/services/ai/anthropic.d.ts +0 -7
- package/dist/services/ai/anthropic.d.ts.map +0 -1
- package/dist/services/ai/anthropic.js +0 -39
- package/dist/services/ai/anthropic.js.map +0 -1
- package/dist/services/generator.d.ts +0 -2
- package/dist/services/generator.d.ts.map +0 -1
- package/dist/services/generator.js +0 -4
- package/dist/services/generator.js.map +0 -1
- package/dist/services/learner.d.ts +0 -2
- package/dist/services/learner.d.ts.map +0 -1
- package/dist/services/learner.js +0 -4
- package/dist/services/learner.js.map +0 -1
- package/dist/services/scanner.d.ts +0 -3
- package/dist/services/scanner.d.ts.map +0 -1
- package/dist/services/scanner.js +0 -54
- package/dist/services/scanner.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -15
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -27
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/file.d.ts +0 -7
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/file.js +0 -32
- package/dist/utils/file.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -17
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/path.d.ts +0 -6
- package/dist/utils/path.d.ts.map +0 -1
- package/dist/utils/path.js +0 -14
- package/dist/utils/path.js.map +0 -1
- package/docs/planning/memory-lane.md +0 -83
- package/packaging/linux/aicgen.spec +0 -23
- package/packaging/linux/control +0 -9
- package/packaging/macos/scripts/postinstall +0 -12
- package/packaging/windows/setup.nsi +0 -92
- package/scripts/add-categories.ts +0 -87
- package/scripts/build-binary.ts +0 -46
- package/scripts/embed-data.ts +0 -105
- package/scripts/generate-version.ts +0 -150
- package/scripts/test-decompress.ts +0 -27
- package/scripts/test-extract.ts +0 -31
- package/src/__tests__/services/assistant-file-writer.test.ts +0 -400
- package/src/__tests__/services/guideline-loader.test.ts +0 -281
- package/src/__tests__/services/tarball-extraction.test.ts +0 -125
- package/src/commands/add-guideline.ts +0 -296
- package/src/commands/clear.ts +0 -61
- package/src/commands/guideline-selector.ts +0 -123
- package/src/commands/init.ts +0 -645
- package/src/commands/quick-add.ts +0 -586
- package/src/commands/remove-guideline.ts +0 -152
- package/src/commands/stats.ts +0 -49
- package/src/commands/update.ts +0 -240
- package/src/config.ts +0 -82
- package/src/embedded-data.ts +0 -1492
- package/src/index.ts +0 -67
- package/src/models/profile.ts +0 -24
- package/src/models/project.ts +0 -43
- package/src/services/assistant-file-writer.ts +0 -612
- package/src/services/config-generator.ts +0 -150
- package/src/services/config-manager.ts +0 -70
- package/src/services/data-source.ts +0 -248
- package/src/services/first-run-init.ts +0 -148
- package/src/services/guideline-loader.ts +0 -311
- package/src/services/hook-generator.ts +0 -178
- package/src/services/subagent-generator.ts +0 -310
- package/src/utils/banner.ts +0 -66
- package/src/utils/errors.ts +0 -27
- package/src/utils/file.ts +0 -67
- package/src/utils/formatting.ts +0 -172
- package/src/utils/logger.ts +0 -89
- package/src/utils/path.ts +0 -17
- package/src/utils/wizard-state.ts +0 -132
- package/tsconfig.json +0 -25
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
# Swift Error Handling
|
|
2
|
+
|
|
3
|
+
## Error Types
|
|
4
|
+
|
|
5
|
+
Define custom error types using enums conforming to Error:
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
// ✅ Simple error enum
|
|
9
|
+
enum NetworkError: Error {
|
|
10
|
+
case connectionFailed
|
|
11
|
+
case timeout
|
|
12
|
+
case invalidResponse
|
|
13
|
+
case unauthorized
|
|
14
|
+
case serverError(code: Int)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ✅ Error with associated values
|
|
18
|
+
enum ValidationError: Error {
|
|
19
|
+
case emptyField(fieldName: String)
|
|
20
|
+
case invalidFormat(fieldName: String, reason: String)
|
|
21
|
+
case tooShort(fieldName: String, minimumLength: Int)
|
|
22
|
+
case tooLong(fieldName: String, maximumLength: Int)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ✅ Error with localized descriptions
|
|
26
|
+
enum AuthError: Error {
|
|
27
|
+
case invalidCredentials
|
|
28
|
+
case accountLocked
|
|
29
|
+
case sessionExpired
|
|
30
|
+
|
|
31
|
+
var localizedDescription: String {
|
|
32
|
+
switch self {
|
|
33
|
+
case .invalidCredentials:
|
|
34
|
+
return "The email or password you entered is incorrect."
|
|
35
|
+
case .accountLocked:
|
|
36
|
+
return "Your account has been temporarily locked. Please try again later."
|
|
37
|
+
case .sessionExpired:
|
|
38
|
+
return "Your session has expired. Please log in again."
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ❌ Avoid overly generic errors
|
|
44
|
+
enum GenericError: Error {
|
|
45
|
+
case error
|
|
46
|
+
case failed
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Throwing Functions
|
|
51
|
+
|
|
52
|
+
Use `throws` to indicate a function can throw an error:
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
// ✅ Throwing function
|
|
56
|
+
func fetchUser(id: String) throws -> User {
|
|
57
|
+
guard !id.isEmpty else {
|
|
58
|
+
throw ValidationError.emptyField(fieldName: "id")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
guard let user = database.findUser(id: id) else {
|
|
62
|
+
throw DatabaseError.notFound
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return user
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ✅ Async throwing function
|
|
69
|
+
func fetchUserAsync(id: String) async throws -> User {
|
|
70
|
+
let response = try await URLSession.shared.data(from: url)
|
|
71
|
+
return try JSONDecoder().decode(User.self, from: response.0)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ✅ Rethrows - propagates errors from closure
|
|
75
|
+
func transform<T, U>(_ items: [T], using: (T) throws -> U) rethrows -> [U] {
|
|
76
|
+
var results: [U] = []
|
|
77
|
+
for item in items {
|
|
78
|
+
let transformed = try using(item)
|
|
79
|
+
results.append(transformed)
|
|
80
|
+
}
|
|
81
|
+
return results
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Handling Errors with do-catch
|
|
86
|
+
|
|
87
|
+
```swift
|
|
88
|
+
// ✅ Basic error handling
|
|
89
|
+
func loadUser() {
|
|
90
|
+
do {
|
|
91
|
+
let user = try fetchUser(id: "123")
|
|
92
|
+
print("User: \(user.name)")
|
|
93
|
+
} catch {
|
|
94
|
+
print("Error: \(error)")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ✅ Catch specific errors
|
|
99
|
+
func handleSpecificErrors() {
|
|
100
|
+
do {
|
|
101
|
+
let user = try fetchUser(id: "123")
|
|
102
|
+
print(user.name)
|
|
103
|
+
} catch NetworkError.connectionFailed {
|
|
104
|
+
print("No internet connection")
|
|
105
|
+
} catch NetworkError.timeout {
|
|
106
|
+
print("Request timed out")
|
|
107
|
+
} catch NetworkError.serverError(let code) {
|
|
108
|
+
print("Server error: \(code)")
|
|
109
|
+
} catch {
|
|
110
|
+
print("Unknown error: \(error)")
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ✅ Async error handling
|
|
115
|
+
func loadUserAsync() async {
|
|
116
|
+
do {
|
|
117
|
+
let user = try await fetchUserAsync(id: "123")
|
|
118
|
+
print("User: \(user.name)")
|
|
119
|
+
} catch {
|
|
120
|
+
print("Failed to load user: \(error)")
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ❌ Don't catch and ignore errors silently
|
|
125
|
+
func badErrorHandling() {
|
|
126
|
+
do {
|
|
127
|
+
try riskyOperation()
|
|
128
|
+
} catch {
|
|
129
|
+
// Silent failure - bad practice!
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Try Variants
|
|
135
|
+
|
|
136
|
+
Swift provides three variants of `try`:
|
|
137
|
+
|
|
138
|
+
```swift
|
|
139
|
+
// ✅ try - Must be in do-catch or throwing function
|
|
140
|
+
func normalTry() throws {
|
|
141
|
+
let user = try fetchUser(id: "123")
|
|
142
|
+
process(user)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ✅ try? - Converts error to optional (nil on error)
|
|
146
|
+
func optionalTry() {
|
|
147
|
+
if let user = try? fetchUser(id: "123") {
|
|
148
|
+
print("User: \(user.name)")
|
|
149
|
+
} else {
|
|
150
|
+
print("Failed to fetch user")
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ⚠️ try! - Force try (crashes on error) - Use sparingly
|
|
155
|
+
func forceTry() {
|
|
156
|
+
let config = try! loadConfig() // Only if error is truly impossible
|
|
157
|
+
print(config.apiKey)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ✅ Good use of try! - Error is truly impossible
|
|
161
|
+
func loadBundledFile() -> String {
|
|
162
|
+
guard let url = Bundle.main.url(forResource: "data", withExtension: "json"),
|
|
163
|
+
let data = try? Data(contentsOf: url),
|
|
164
|
+
let string = String(data: data, encoding: .utf8) else {
|
|
165
|
+
fatalError("Bundled file must exist")
|
|
166
|
+
}
|
|
167
|
+
return string
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ❌ Bad use of try! - Error is possible
|
|
171
|
+
func badForceTry() {
|
|
172
|
+
let user = try! fetchUser(id: "123") // Crashes if fetch fails!
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Result Type
|
|
177
|
+
|
|
178
|
+
Use Result for explicit success/failure without exceptions:
|
|
179
|
+
|
|
180
|
+
```swift
|
|
181
|
+
// ✅ Return Result instead of throwing
|
|
182
|
+
func fetchUser(id: String) -> Result<User, Error> {
|
|
183
|
+
guard !id.isEmpty else {
|
|
184
|
+
return .failure(ValidationError.emptyField(fieldName: "id"))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
do {
|
|
188
|
+
let user = try database.findUser(id: id)
|
|
189
|
+
return .success(user)
|
|
190
|
+
} catch {
|
|
191
|
+
return .failure(error)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ✅ Handle Result with switch
|
|
196
|
+
func handleResult() {
|
|
197
|
+
let result = fetchUser(id: "123")
|
|
198
|
+
|
|
199
|
+
switch result {
|
|
200
|
+
case .success(let user):
|
|
201
|
+
print("User: \(user.name)")
|
|
202
|
+
case .failure(let error):
|
|
203
|
+
print("Error: \(error)")
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ✅ Use Result methods
|
|
208
|
+
func useResultMethods() {
|
|
209
|
+
let result = fetchUser(id: "123")
|
|
210
|
+
|
|
211
|
+
// Get value or provide default
|
|
212
|
+
let user = result.get(default: User.guest)
|
|
213
|
+
|
|
214
|
+
// Map success value
|
|
215
|
+
let userName = result.map { $0.name }
|
|
216
|
+
|
|
217
|
+
// Map error
|
|
218
|
+
let friendly = result.mapError { error in
|
|
219
|
+
"Failed to load user: \(error.localizedDescription)"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ✅ Convert between Result and throws
|
|
224
|
+
func convertToThrowing() throws -> User {
|
|
225
|
+
let result = fetchUser(id: "123")
|
|
226
|
+
return try result.get() // Throws if failure
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
func convertFromThrowing() -> Result<User, Error> {
|
|
230
|
+
Result { try fetchUserThrowing(id: "123") }
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## When to Use Throws vs Result
|
|
235
|
+
|
|
236
|
+
### Use `throws` when:
|
|
237
|
+
|
|
238
|
+
```swift
|
|
239
|
+
// ✅ Synchronous operations where errors are exceptional
|
|
240
|
+
func parseJSON<T: Decodable>(_ data: Data) throws -> T {
|
|
241
|
+
try JSONDecoder().decode(T.self, from: data)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ✅ Async operations
|
|
245
|
+
func fetchData() async throws -> Data {
|
|
246
|
+
try await URLSession.shared.data(from: url).0
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ✅ Errors should propagate automatically
|
|
250
|
+
func processOrder() throws {
|
|
251
|
+
try validateOrder()
|
|
252
|
+
try chargePayment()
|
|
253
|
+
try shipOrder()
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Use `Result` when:
|
|
258
|
+
|
|
259
|
+
```swift
|
|
260
|
+
// ✅ Storing error state
|
|
261
|
+
class ViewModel: ObservableObject {
|
|
262
|
+
@Published var userResult: Result<User, Error>?
|
|
263
|
+
|
|
264
|
+
func loadUser() async {
|
|
265
|
+
userResult = await fetchUserResult(id: "123")
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ✅ Callback-based APIs
|
|
270
|
+
func fetchUser(completion: @escaping (Result<User, Error>) -> Void) {
|
|
271
|
+
// Async operation
|
|
272
|
+
DispatchQueue.global().async {
|
|
273
|
+
let result = self.performFetch()
|
|
274
|
+
DispatchQueue.main.async {
|
|
275
|
+
completion(result)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ✅ When you want to delay error handling
|
|
281
|
+
func loadUsers() -> [Result<User, Error>] {
|
|
282
|
+
userIDs.map { id in
|
|
283
|
+
Result { try fetchUser(id: id) }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Error Recovery Patterns
|
|
289
|
+
|
|
290
|
+
### Retry with Exponential Backoff
|
|
291
|
+
|
|
292
|
+
```swift
|
|
293
|
+
// ✅ Retry failing operations
|
|
294
|
+
func fetchWithRetry<T>(
|
|
295
|
+
maxAttempts: Int = 3,
|
|
296
|
+
operation: @escaping () async throws -> T
|
|
297
|
+
) async throws -> T {
|
|
298
|
+
var lastError: Error?
|
|
299
|
+
|
|
300
|
+
for attempt in 0..<maxAttempts {
|
|
301
|
+
do {
|
|
302
|
+
return try await operation()
|
|
303
|
+
} catch {
|
|
304
|
+
lastError = error
|
|
305
|
+
|
|
306
|
+
if attempt < maxAttempts - 1 {
|
|
307
|
+
let delay = UInt64(pow(2.0, Double(attempt)) * 1_000_000_000)
|
|
308
|
+
try await Task.sleep(nanoseconds: delay)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
throw lastError!
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Usage
|
|
317
|
+
let user = try await fetchWithRetry {
|
|
318
|
+
try await fetchUser(id: "123")
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Fallback Values
|
|
323
|
+
|
|
324
|
+
```swift
|
|
325
|
+
// ✅ Provide fallback on error
|
|
326
|
+
func loadUserWithFallback(id: String) -> User {
|
|
327
|
+
do {
|
|
328
|
+
return try fetchUser(id: id)
|
|
329
|
+
} catch {
|
|
330
|
+
return User.guest
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ✅ Try multiple sources
|
|
335
|
+
func loadConfiguration() -> Configuration {
|
|
336
|
+
// Try remote config first
|
|
337
|
+
if let config = try? fetchRemoteConfig() {
|
|
338
|
+
return config
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Fall back to cached config
|
|
342
|
+
if let config = try? loadCachedConfig() {
|
|
343
|
+
return config
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Last resort: default config
|
|
347
|
+
return Configuration.default
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Partial Success
|
|
352
|
+
|
|
353
|
+
```swift
|
|
354
|
+
// ✅ Return partial results with errors
|
|
355
|
+
func fetchAllUsers(ids: [String]) async -> (users: [User], errors: [Error]) {
|
|
356
|
+
var users: [User] = []
|
|
357
|
+
var errors: [Error] = []
|
|
358
|
+
|
|
359
|
+
await withTaskGroup(of: Result<User, Error>.self) { group in
|
|
360
|
+
for id in ids {
|
|
361
|
+
group.addTask {
|
|
362
|
+
do {
|
|
363
|
+
return .success(try await fetchUser(id: id))
|
|
364
|
+
} catch {
|
|
365
|
+
return .failure(error)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
for await result in group {
|
|
371
|
+
switch result {
|
|
372
|
+
case .success(let user):
|
|
373
|
+
users.append(user)
|
|
374
|
+
case .failure(let error):
|
|
375
|
+
errors.append(error)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return (users, errors)
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Custom Error Context
|
|
385
|
+
|
|
386
|
+
Add context to errors:
|
|
387
|
+
|
|
388
|
+
```swift
|
|
389
|
+
// ✅ Wrap errors with context
|
|
390
|
+
struct ContextualError: Error {
|
|
391
|
+
let underlyingError: Error
|
|
392
|
+
let context: String
|
|
393
|
+
let metadata: [String: Any]
|
|
394
|
+
|
|
395
|
+
init(_ error: Error, context: String, metadata: [String: Any] = [:]) {
|
|
396
|
+
self.underlyingError = error
|
|
397
|
+
self.context = context
|
|
398
|
+
self.metadata = metadata
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
func fetchUserWithContext(id: String) throws -> User {
|
|
403
|
+
do {
|
|
404
|
+
return try fetchUser(id: id)
|
|
405
|
+
} catch {
|
|
406
|
+
throw ContextualError(
|
|
407
|
+
error,
|
|
408
|
+
context: "Failed to fetch user",
|
|
409
|
+
metadata: ["userId": id, "timestamp": Date()]
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ✅ Add context without wrapping
|
|
415
|
+
extension Error {
|
|
416
|
+
func addingContext(_ context: String) -> Error {
|
|
417
|
+
ContextualError(self, context: context)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
func processOrder() throws {
|
|
422
|
+
do {
|
|
423
|
+
try chargePayment()
|
|
424
|
+
} catch {
|
|
425
|
+
throw error.addingContext("Payment processing failed")
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Error Logging
|
|
431
|
+
|
|
432
|
+
```swift
|
|
433
|
+
// ✅ Structured error logging
|
|
434
|
+
func logError(_ error: Error, context: String, metadata: [String: Any] = [:]) {
|
|
435
|
+
var logData: [String: Any] = [
|
|
436
|
+
"error": String(describing: error),
|
|
437
|
+
"context": context,
|
|
438
|
+
"timestamp": Date().ISO8601Format()
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
logData.merge(metadata) { (_, new) in new }
|
|
442
|
+
|
|
443
|
+
// Log to your logging system
|
|
444
|
+
print("ERROR: \(logData)")
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Usage
|
|
448
|
+
func handleError() {
|
|
449
|
+
do {
|
|
450
|
+
try riskyOperation()
|
|
451
|
+
} catch {
|
|
452
|
+
logError(error, context: "Risk operation failed", metadata: [
|
|
453
|
+
"userId": currentUserId,
|
|
454
|
+
"operation": "riskyOperation"
|
|
455
|
+
])
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ✅ Error boundary pattern
|
|
460
|
+
func performWithErrorBoundary<T>(
|
|
461
|
+
operation: () throws -> T,
|
|
462
|
+
onError: (Error) -> Void
|
|
463
|
+
) -> T? {
|
|
464
|
+
do {
|
|
465
|
+
return try operation()
|
|
466
|
+
} catch {
|
|
467
|
+
logError(error, context: "Operation failed")
|
|
468
|
+
onError(error)
|
|
469
|
+
return nil
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## Preconditions and Assertions
|
|
475
|
+
|
|
476
|
+
For programmer errors, use preconditions instead of throwing:
|
|
477
|
+
|
|
478
|
+
```swift
|
|
479
|
+
// ✅ Precondition for programmer errors
|
|
480
|
+
func divide(_ a: Int, by b: Int) -> Int {
|
|
481
|
+
precondition(b != 0, "Division by zero")
|
|
482
|
+
return a / b
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ✅ Assert in debug builds only
|
|
486
|
+
func process(_ items: [Item]) {
|
|
487
|
+
assert(!items.isEmpty, "Items should not be empty")
|
|
488
|
+
// Process items
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ✅ Fatal error for unrecoverable errors
|
|
492
|
+
func loadRequiredConfiguration() -> Configuration {
|
|
493
|
+
guard let config = try? loadConfiguration() else {
|
|
494
|
+
fatalError("Failed to load required configuration")
|
|
495
|
+
}
|
|
496
|
+
return config
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ❌ Don't use throwing for programmer errors
|
|
500
|
+
func badDivide(_ a: Int, by b: Int) throws -> Int {
|
|
501
|
+
guard b != 0 else {
|
|
502
|
+
throw MathError.divisionByZero // Should be precondition
|
|
503
|
+
}
|
|
504
|
+
return a / b
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## Error Handling Best Practices
|
|
509
|
+
|
|
510
|
+
### Be Specific with Error Types
|
|
511
|
+
|
|
512
|
+
```swift
|
|
513
|
+
// ❌ Generic error
|
|
514
|
+
enum AppError: Error {
|
|
515
|
+
case error
|
|
516
|
+
case somethingWentWrong
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ✅ Specific errors
|
|
520
|
+
enum UserServiceError: Error {
|
|
521
|
+
case userNotFound(id: String)
|
|
522
|
+
case invalidEmail(String)
|
|
523
|
+
case duplicateEmail(String)
|
|
524
|
+
case insufficientPermissions(required: Permission, actual: Permission)
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Handle Errors at the Right Level
|
|
529
|
+
|
|
530
|
+
```swift
|
|
531
|
+
// ✅ Handle errors where you have context
|
|
532
|
+
func loadUserProfile() async {
|
|
533
|
+
do {
|
|
534
|
+
let user = try await fetchUser(id: userId)
|
|
535
|
+
self.user = user
|
|
536
|
+
} catch NetworkError.unauthorized {
|
|
537
|
+
// Handle at this level - we know what to do
|
|
538
|
+
showLoginScreen()
|
|
539
|
+
} catch {
|
|
540
|
+
// Let other errors propagate or show generic error
|
|
541
|
+
showErrorAlert(error.localizedDescription)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ❌ Don't catch errors you can't handle
|
|
546
|
+
func badErrorHandling() {
|
|
547
|
+
do {
|
|
548
|
+
try fetchUser(id: "123")
|
|
549
|
+
} catch {
|
|
550
|
+
print("Error!") // What now? No useful handling
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Provide User-Friendly Messages
|
|
556
|
+
|
|
557
|
+
```swift
|
|
558
|
+
// ✅ Convert technical errors to user messages
|
|
559
|
+
extension Error {
|
|
560
|
+
var userFriendlyMessage: String {
|
|
561
|
+
switch self {
|
|
562
|
+
case NetworkError.connectionFailed:
|
|
563
|
+
return "Please check your internet connection and try again."
|
|
564
|
+
case NetworkError.timeout:
|
|
565
|
+
return "The request took too long. Please try again."
|
|
566
|
+
case NetworkError.unauthorized:
|
|
567
|
+
return "Your session has expired. Please log in again."
|
|
568
|
+
case let ValidationError.invalidFormat(field, _):
|
|
569
|
+
return "The \(field) format is invalid. Please check and try again."
|
|
570
|
+
default:
|
|
571
|
+
return "Something went wrong. Please try again later."
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Usage
|
|
577
|
+
do {
|
|
578
|
+
try performOperation()
|
|
579
|
+
} catch {
|
|
580
|
+
showAlert(message: error.userFriendlyMessage)
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Document Error Conditions
|
|
585
|
+
|
|
586
|
+
```swift
|
|
587
|
+
// ✅ Document what errors can be thrown
|
|
588
|
+
/// Fetches a user from the API.
|
|
589
|
+
///
|
|
590
|
+
/// - Parameter id: The unique identifier for the user
|
|
591
|
+
/// - Returns: The user object
|
|
592
|
+
/// - Throws:
|
|
593
|
+
/// - `NetworkError.connectionFailed` if no internet connection
|
|
594
|
+
/// - `NetworkError.unauthorized` if authentication fails
|
|
595
|
+
/// - `NetworkError.notFound` if user doesn't exist
|
|
596
|
+
/// - `DecodingError` if response parsing fails
|
|
597
|
+
func fetchUser(id: String) async throws -> User {
|
|
598
|
+
// Implementation
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## Async Error Handling Patterns
|
|
603
|
+
|
|
604
|
+
### Structured Concurrency Errors
|
|
605
|
+
|
|
606
|
+
```swift
|
|
607
|
+
// ✅ First error cancels sibling tasks
|
|
608
|
+
func fetchDashboard() async throws -> Dashboard {
|
|
609
|
+
try await withThrowingTaskGroup(of: DashboardData.self) { group in
|
|
610
|
+
group.addTask { try await fetchUserData() }
|
|
611
|
+
group.addTask { try await fetchPosts() }
|
|
612
|
+
group.addTask { try await fetchStats() }
|
|
613
|
+
|
|
614
|
+
var results: [DashboardData] = []
|
|
615
|
+
for try await result in group {
|
|
616
|
+
results.append(result)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return Dashboard(data: results)
|
|
620
|
+
}
|
|
621
|
+
// If any task throws, others are automatically cancelled
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ✅ Collect all results, including errors
|
|
625
|
+
func fetchAllData() async -> [Result<Data, Error>] {
|
|
626
|
+
await withTaskGroup(of: Result<Data, Error>.self) { group in
|
|
627
|
+
for url in urls {
|
|
628
|
+
group.addTask {
|
|
629
|
+
do {
|
|
630
|
+
return .success(try await fetch(from: url))
|
|
631
|
+
} catch {
|
|
632
|
+
return .failure(error)
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
var results: [Result<Data, Error>] = []
|
|
638
|
+
for await result in group {
|
|
639
|
+
results.append(result)
|
|
640
|
+
}
|
|
641
|
+
return results
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Cancellation as Error
|
|
647
|
+
|
|
648
|
+
```swift
|
|
649
|
+
// ✅ Handle task cancellation
|
|
650
|
+
func performLongOperation() async throws -> Result {
|
|
651
|
+
for i in 0..<1000 {
|
|
652
|
+
// Check for cancellation
|
|
653
|
+
try Task.checkCancellation()
|
|
654
|
+
|
|
655
|
+
await processItem(i)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return result
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ✅ Graceful cancellation handling
|
|
662
|
+
func downloadWithCancellation() async throws -> Data {
|
|
663
|
+
do {
|
|
664
|
+
return try await performDownload()
|
|
665
|
+
} catch is CancellationError {
|
|
666
|
+
// Clean up partial download
|
|
667
|
+
cleanupPartialData()
|
|
668
|
+
throw DownloadError.cancelled
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
**Sources:**
|
|
676
|
+
- [Swift.org: Error Handling](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html)
|
|
677
|
+
- [Apple Documentation: Error Handling](https://developer.apple.com/documentation/swift/error)
|
|
678
|
+
- [Swift by Sundell: Error Handling](https://www.swiftbysundell.com/basics/error-handling/)
|
|
679
|
+
- [WWDC: Modern Swift Error Handling](https://developer.apple.com/videos/play/wwdc2020/10672/)
|