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!