Lesson 90Error Handling

Error Handling Practice

Module Summary

Congratulations on completing the Error Handling module! You now know how to write robust, error-resistant Swift code.

What You've Learned

Error Types

Enum, LocalizedError

throws/try

Throwing functions & handling

do-try-catch

Error handling blocks

try? / try!

Optional & forced try

defer

Guaranteed cleanup

Result Type

Success/Failure enum

Failable Init

init? for validation

Assertions

Debug-time checks

Practice Exercises

1. File Processing System

Build error types with LocalizedError for file operations.

2. Network Layer with Result

Implement async API calls returning Result type.

3. Form Validation

Chain multiple validations with throws.

4. Database Operations

Use defer for transaction cleanup.

5. Custom Validated Type

Build your own Result-like validation type.

Key Takeaways

  • Be specific - Use descriptive error types
  • Provide context - Associated values help debugging
  • Handle or propagate - Don't swallow errors
  • Clean up - Use defer for resource management
  • Test failures - Error paths need testing too
practice.swift
// ERROR HANDLING PRACTICE
// Comprehensive exercises for all error handling concepts

// ===== EXERCISE 1: File Processing System =====
enum FileProcessingError: LocalizedError {
    case fileNotFound(path: String)
    case invalidFormat(expected: String)
    case permissionDenied
    case fileTooLarge(size: Int, maxSize: Int)

    var errorDescription: String? {
        switch self {
        case .fileNotFound(let path):
            return "File not found: \(path)"
        case .invalidFormat(let expected):
            return "Invalid format. Expected: \(expected)"
        case .permissionDenied:
            return "Permission denied"
        case .fileTooLarge(let size, let maxSize):
            return "File too large: \(size) bytes (max: \(maxSize))"
        }
    }
}

class FileProcessor {
    let maxFileSize = 10_000_000  // 10MB

    func processFile(at path: String) throws -> String {
        // Check file exists
        guard fileExists(path) else {
            throw FileProcessingError.fileNotFound(path: path)
        }

        // Check permissions
        guard hasPermission(path) else {
            throw FileProcessingError.permissionDenied
        }

        // Check size
        let size = getFileSize(path)
        guard size <= maxFileSize else {
            throw FileProcessingError.fileTooLarge(size: size, maxSize: maxFileSize)
        }

        // Read and process
        return "Processed content"
    }

    func fileExists(_ path: String) -> Bool { true }
    func hasPermission(_ path: String) -> Bool { true }
    func getFileSize(_ path: String) -> Int { 1000 }
}

// ===== EXERCISE 2: Network Layer with Result =====
enum NetworkError: Error {
    case noConnection
    case timeout
    case serverError(code: Int)
    case decodingFailed
}

struct APIResponse<T: Codable>: Codable {
    let success: Bool
    let data: T?
    let error: String?
}

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

class NetworkManager {
    func fetchUser(id: Int, completion: @escaping (Result<User, NetworkError>) -> Void) {
        // Simulate async network call
        DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
            if id > 0 {
                let user = User(id: id, name: "John Doe", email: "[email protected]")
                completion(.success(user))
            } else {
                completion(.failure(.serverError(code: 404)))
            }
        }
    }

    func fetchUsers() -> Result<[User], NetworkError> {
        // Synchronous version
        return .success([
            User(id: 1, name: "Alice", email: "[email protected]"),
            User(id: 2, name: "Bob", email: "[email protected]")
        ])
    }
}

// ===== EXERCISE 3: Form Validation =====
struct FormValidator {
    enum ValidationError: Error {
        case emptyField(name: String)
        case invalidEmail
        case passwordTooShort(minimum: Int)
        case passwordsDoNotMatch
        case invalidAge(min: Int, max: Int)
    }

    func validateEmail(_ email: String) throws {
        guard !email.isEmpty else {
            throw ValidationError.emptyField(name: "email")
        }
        guard email.contains("@"), email.contains(".") else {
            throw ValidationError.invalidEmail
        }
    }

    func validatePassword(_ password: String, confirm: String) throws {
        guard password.count >= 8 else {
            throw ValidationError.passwordTooShort(minimum: 8)
        }
        guard password == confirm else {
            throw ValidationError.passwordsDoNotMatch
        }
    }

    func validateAge(_ age: Int) throws {
        guard age >= 13, age <= 120 else {
            throw ValidationError.invalidAge(min: 13, max: 120)
        }
    }

    func validateForm(email: String, password: String, confirm: String, age: Int) throws {
        try validateEmail(email)
        try validatePassword(password, confirm: confirm)
        try validateAge(age)
    }
}

// Usage
let validator = FormValidator()
do {
    try validator.validateForm(
        email: "[email protected]",
        password: "secure123",
        confirm: "secure123",
        age: 25
    )
    print("Form is valid!")
} catch let error as FormValidator.ValidationError {
    switch error {
    case .emptyField(let name):
        print("\(name) is required")
    case .invalidEmail:
        print("Please enter a valid email")
    case .passwordTooShort(let min):
        print("Password must be at least \(min) characters")
    case .passwordsDoNotMatch:
        print("Passwords don't match")
    case .invalidAge(let min, let max):
        print("Age must be between \(min) and \(max)")
    }
}

// ===== EXERCISE 4: Database Operations =====
protocol DatabaseError: Error {
    var isRecoverable: Bool { get }
}

enum SQLiteError: DatabaseError {
    case connectionFailed
    case queryFailed(sql: String)
    case constraintViolation(field: String)
    case transactionFailed

    var isRecoverable: Bool {
        switch self {
        case .connectionFailed: return true
        case .queryFailed: return false
        case .constraintViolation: return false
        case .transactionFailed: return true
        }
    }
}

class Database {
    var isConnected = false

    func connect() throws {
        defer { print("Connection attempt completed") }
        isConnected = true
    }

    func executeInTransaction(_ operations: () throws -> Void) throws {
        print("BEGIN TRANSACTION")

        defer {
            if !isConnected {
                print("ROLLBACK")
            }
        }

        try operations()
        print("COMMIT")
    }
}

// ===== EXERCISE 5: Custom Result Type =====
enum Validated<T> {
    case valid(T)
    case invalid([String])

    func map<U>(_ transform: (T) -> U) -> Validated<U> {
        switch self {
        case .valid(let value):
            return .valid(transform(value))
        case .invalid(let errors):
            return .invalid(errors)
        }
    }
}

// Usage
func validateUsername(_ name: String) -> Validated<String> {
    var errors: [String] = []

    if name.isEmpty {
        errors.append("Username cannot be empty")
    }
    if name.count < 3 {
        errors.append("Username must be at least 3 characters")
    }
    if name.contains(" ") {
        errors.append("Username cannot contain spaces")
    }

    return errors.isEmpty ? .valid(name) : .invalid(errors)
}

let result = validateUsername("ab")
switch result {
case .valid(let username):
    print("Valid: \(username)")
case .invalid(let errors):
    print("Errors: \(errors)")
}

Module Complete!

You've mastered Swift Error Handling! You can now write robust, user-friendly code that gracefully handles failures.

Next up: Advanced Topics - async/await, memory management, and more!