Lesson 89Error Handling
Error Handling Best Practices
Writing Robust Code
Good error handling makes your app reliable and user-friendly. These best practices will help you handle errors effectively.
Key Principles
Be Specific
Use descriptive error types
Provide Context
Use associated values
Don't Swallow
Handle or propagate
Clean Up
Use defer statements
Checklist
- ✓Use specific error types with associated values
- ✓Implement LocalizedError for user-facing errors
- ✓Don't silently catch and ignore errors
- ✓Use defer for guaranteed cleanup
- ✓Transform errors at layer boundaries
- ✓Avoid try! except for impossible failures
- ✓Log errors for debugging
- ✓Test error paths thoroughly
Common Anti-Patterns
- ✗Empty catch blocks
- ✗Using try! with network/user data
- ✗Generic "Something went wrong" messages
- ✗Not testing failure scenarios
best_practices.swift
// ERROR HANDLING BEST PRACTICES
// Guidelines for robust Swift applications
// ===== 1. USE SPECIFIC ERROR TYPES =====
// Bad: Generic error
enum BadError: Error {
case somethingWentWrong
}
// Good: Specific, descriptive errors
enum NetworkError: Error {
case noConnection
case timeout(seconds: Int)
case serverError(statusCode: Int)
case invalidResponse(data: Data)
}
// ===== 2. PROVIDE CONTEXT WITH ASSOCIATED VALUES =====
enum FileError: Error {
case notFound(path: String)
case permissionDenied(path: String, requiredPermission: String)
case readFailed(path: String, underlyingError: Error)
}
// ===== 3. IMPLEMENT LOCALIZEDERROR FOR USER-FACING =====
enum ValidationError: LocalizedError {
case invalidEmail(String)
case passwordTooWeak
var errorDescription: String? {
switch self {
case .invalidEmail(let email):
return "\(email) is not a valid email address"
case .passwordTooWeak:
return "Password must be at least 8 characters"
}
}
var recoverySuggestion: String? {
switch self {
case .invalidEmail:
return "Please enter a valid email like [email protected]"
case .passwordTooWeak:
return "Include numbers and special characters"
}
}
}
// ===== 4. DON'T CATCH ERRORS YOU CAN'T HANDLE =====
// Bad: Catching and ignoring
func badExample() {
do {
try riskyOperation()
} catch {
// Silently ignored - BAD!
}
}
// Good: Propagate or handle meaningfully
func goodExample() throws {
try riskyOperation() // Let caller decide
}
func riskyOperation() throws { }
// ===== 5. USE RESULT FOR ASYNC OPERATIONS =====
// Before async/await, use Result for clarity
func fetchUser(id: Int, completion: @escaping (Result<User, NetworkError>) -> Void) {
// ...
}
struct User { let id: Int; let name: String }
// ===== 6. TRANSFORM ERRORS AT BOUNDARIES =====
enum AppError: Error {
case network(NetworkError)
case validation(ValidationError)
case unknown(Error)
}
func wrapError(_ error: Error) -> AppError {
if let networkError = error as? NetworkError {
return .network(networkError)
}
if let validationError = error as? ValidationError {
return .validation(validationError)
}
return .unknown(error)
}
// ===== 7. USE DEFER FOR CLEANUP =====
func processFile(at path: String) throws {
let file = openFile(path)
defer {
closeFile(file) // Always executed!
}
try readContents(file)
try processContents(file)
}
func openFile(_ path: String) -> Int { return 0 }
func closeFile(_ file: Int) { }
func readContents(_ file: Int) throws { }
func processContents(_ file: Int) throws { }
// ===== 8. AVOID TRY! IN PRODUCTION CODE =====
// Bad: Can crash
// let data = try! loadCriticalFile()
// Good: Handle the error
func loadWithFallback() -> Data {
if let data = try? loadCriticalFile() {
return data
}
return Data() // Fallback
}
func loadCriticalFile() throws -> Data { return Data() }
// ===== 9. LOG ERRORS FOR DEBUGGING =====
func handleError(_ error: Error) {
// Log for debugging
print("[ERROR] \(type(of: error)): \(error.localizedDescription)")
// Show user-friendly message
if let localizedError = error as? LocalizedError {
showAlert(localizedError.errorDescription ?? "An error occurred")
} else {
showAlert("Something went wrong. Please try again.")
}
}
func showAlert(_ message: String) {
print("Alert: \(message)")
}
// ===== 10. TEST ERROR PATHS =====
// Always test both success and failure cases
func testFetchUser() {
// Test success
let successResult = fetchUserSync(id: 1)
assert(successResult != nil)
// Test failure
let failureResult = fetchUserSync(id: -1)
assert(failureResult == nil)
}
func fetchUserSync(id: Int) -> User? {
return id > 0 ? User(id: id, name: "Test") : nil
}
// ===== SUMMARY CHECKLIST =====
// ✓ Use specific error types with associated values
// ✓ Implement LocalizedError for user-facing errors
// ✓ Don't swallow errors silently
// ✓ Use defer for cleanup
// ✓ Transform errors at layer boundaries
// ✓ Avoid try! except for truly impossible failures
// ✓ Log errors for debugging
// ✓ Test error paths thoroughlyTry It Yourself!
Review one of your existing projects and apply these best practices. Look for empty catch blocks, try! usage, and generic error messages!