Skip to content

Commit 87e3088

Browse files
committed
Add new section in README documenting the Catching protocol
1 parent 647ac47 commit 87e3088

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed

README.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,131 @@ When contributing:
293293
- Follow the existing naming conventions
294294

295295
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.
296+
297+
298+
## Simplified Error Nesting with the `Catching` Protocol
299+
300+
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.
301+
302+
### The Problem with Manual Error Wrapping
303+
304+
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:
305+
306+
```swift
307+
enum ProfileError: Error {
308+
case validationFailed(field: String)
309+
case databaseError(DatabaseError) // Wrapper case needed
310+
case networkError(NetworkError) // Another wrapper case
311+
case fileError(FileError) // Yet another wrapper
312+
}
313+
314+
// And manual error wrapping in code:
315+
do {
316+
try database.fetch(id)
317+
} catch let error as DatabaseError {
318+
throw .databaseError(error)
319+
}
320+
```
321+
322+
This leads to verbose error types and tedious error handling code when attempting to use typed throws.
323+
324+
### The Solution: `Catching` Protocol
325+
326+
ErrorKit's `Catching` protocol provides a single `caught` case that can wrap any error, plus a convenient `catch` function for automatic error wrapping:
327+
328+
```swift
329+
enum ProfileError: Throwable, Catching {
330+
case validationFailed(field: String)
331+
case caught(Error) // Single case handles all nested errors!
332+
}
333+
334+
struct ProfileRepository {
335+
func loadProfile(id: String) throws(ProfileError) {
336+
// Regular error throwing for validation
337+
guard id.isValidFormat else {
338+
throw ProfileError.validationFailed(field: "id")
339+
}
340+
341+
// Automatically wrap any database or file errors
342+
let userData = try ProfileError.catch {
343+
let user = try database.loadUser(id)
344+
let settings = try fileSystem.readUserSettings(user.settingsPath)
345+
return UserProfile(user: user, settings: settings)
346+
}
347+
}
348+
}
349+
```
350+
351+
Note the `ProfileError.catch` function call, which wraps any errors into the `caught` case and also passes through the return type.
352+
353+
### Built-in Support in ErrorKit Types
354+
355+
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:
356+
357+
```swift
358+
func saveUserData() throws(DatabaseError) {
359+
// Automatically wraps SQLite errors, file system errors, etc.
360+
try DatabaseError.catch {
361+
try database.beginTransaction()
362+
try database.execute(query)
363+
try database.commit()
364+
}
365+
}
366+
```
367+
368+
### Adding Catching to Your Error Types
369+
370+
Making your own error types support automatic error wrapping is simple:
371+
372+
1. Conform to the `Catching` protocol
373+
2. Add the `caught(Error)` case to your error type
374+
3. Use the `catch` function for automatic wrapping
375+
376+
```swift
377+
enum AppError: Throwable, Catching {
378+
case invalidConfiguration
379+
case caught(Error) // Required for Catching protocol
380+
381+
var userFriendlyMessage: String {
382+
switch self {
383+
case .invalidConfiguration:
384+
return String(localized: "The app configuration is invalid.")
385+
case .caught(let error):
386+
return error.localizedDescription
387+
}
388+
}
389+
}
390+
391+
// Usage is clean and simple:
392+
func appOperation() throws(AppError) {
393+
// Explicit error throwing for known cases
394+
guard configFileExists else {
395+
throw AppError.invalidConfiguration
396+
}
397+
398+
// Automatic wrapping for system errors and other error types
399+
try AppError.catch {
400+
try riskyOperation()
401+
try anotherRiskyOperation()
402+
}
403+
}
404+
```
405+
406+
### Benefits of Using `Catching`
407+
408+
- **Less Boilerplate**: No need for explicit wrapper cases for each error type
409+
- **Type Safety**: Maintains typed throws while simplifying error handling
410+
- **Clean Code**: Reduces error handling verbosity
411+
- **Automatic Message Propagation**: User-friendly messages flow through the error chain
412+
- **Easy Integration**: Works seamlessly with existing error types
413+
- **Return Value Support**: The `catch` function preserves return values from wrapped operations
414+
415+
### Best Practices
416+
417+
- Use `Catching` for error types that might wrap other errors
418+
- Keep error hierarchies shallow when possible
419+
- Use specific error cases for known errors, `caught` for others
420+
- Preserve user-friendly messages when wrapping errors
421+
- Consider error recovery strategies at each level
422+
423+
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.

0 commit comments

Comments
 (0)