Обработка ошибок в Swift: try/catch и продвинутые техники

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Обработка ошибок в Swift: try/catch и не только (продвинутый уровень)

Обработка ошибок — одна из ключевых тем в разработке на Swift. Если вы уже знакомы с базовым do-catch и try?, настало время перейти на новый уровень. В этой статье мы разберём продвинутые техники: работу с протоколом Error, типом Result, ключевым словом rethrows, а также рассмотрим, как правильно проектировать иерархию ошибок в реальных iOS/macOS проектах.

Глубокое погружение в протокол Error и кастомные ошибки

В Swift любая ошибка должна соответствовать протоколу Error. Однако, чтобы сделать код более читаемым и поддерживаемым, стоит создавать собственные перечисления (enum) с вложенными кейсами и ассоциированными значениями.

enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingFailed(description: String)
    case serverError(statusCode: Int)
    case timeout(seconds: Double)
}

// Пример использования
func fetchUser(id: Int) throws -> User {
    guard id > 0 else {
        throw NetworkError.invalidURL
    }
    // ... логика запроса
    if someCondition {
        throw NetworkError.serverError(statusCode: 500)
    }
    return User()
}

Такой подход позволяет передавать дополнительный контекст: код ошибки, описание или время ожидания. Это особенно полезно при логировании и отладке.

Result: альтернатива do-catch для асинхронного кода

Тип Result<Success, Failure> — мощный инструмент, который пришёл из функционального программирования. Он идеально подходит для callback-based API, сетевых запросов и асинхронных операций.

enum DataError: Error {
    case corrupted
    case notFound
}

func loadImage(from url: URL, completion: @escaping (Result<UIImage, DataError>) -> Void) {
    DispatchQueue.global().async {
        // Симуляция загрузки
        if arc4random_uniform(2) == 0 {
            completion(.success(UIImage()))
        } else {
            completion(.failure(.notFound))
        }
    }
}

// Использование
loadImage(from: someURL) { result in
    switch result {
    case .success(let image):
        print("Изображение загружено: \(image)")
    case .failure(let error):
        print("Ошибка: \(error.localizedDescription)")
    }
}

Result делает код более предсказуемым и избавляет от необходимости обрабатывать опциональные значения с nil. Кроме того, вы можете использовать .map и .flatMap для цепочек трансформаций.

Rethrows: когда функция «пропускает» ошибки

Ключевое слово rethrows — одна из самых недооценённых возможностей Swift. Оно позволяет написать функцию, которая сама не выбрасывает ошибки, но «пробрасывает» их, если переданный замыкание (closure) является throws.

func performOperation<T>(on array: [T], operation: (T) throws -> Bool) rethrows -> [T] {
    var result: [T] = []
    for element in array {
        if try operation(element) {
            result.append(element)
        }
    }
    return result
}

// Без ошибок (не нужно try)
let numbers = [1, 2, 3, 4, 5]
let evenNumbers = performOperation(on: numbers) { $0.isMultiple(of: 2) }
print(evenNumbers) // [2, 4]

// С ошибкой (нужен try)
enum ValidationError: Error { case negative }
let filtered = try performOperation(on: numbers) { value in
    guard value > 0 else { throw ValidationError.negative }
    return value % 2 == 0
}

rethrows делает API гибким: пользователь может передать как обычное замыкание (без try), так и замыкание с throws (тогда потребуется try). Это часто используется в стандартной библиотеке Swift — например, в map, filter и forEach.

Продвинутые паттерны: Error Handling в реальных проектах

В крупных iOS/macOS приложениях ошибки не появляются изолированно. Рассмотрим несколько практических сценариев.

1. Централизованная обработка ошибок

Создайте фабрику или менеджер ошибок, который преобразует низкоуровневые ошибки (например, от URLSession) в доменные ошибки вашего приложения.

enum AppError: Error {
    case network(NetworkError)
    case database(DatabaseError)
    case unknown(String)
}

class ErrorHandler {
    static func handle(_ error: Error) -> AppError {
        if let networkErr = error as? NetworkError {
            return .network(networkErr)
        } else if let dbErr = error as? DatabaseError {
            return .database(dbErr)
        } else {
            return .unknown(error.localizedDescription)
        }
    }
}

2. Использование associated values в enum

Ассоциированные значения позволяют передавать не только код ошибки, но и данные для восстановления.

enum AuthError: Error {
    case invalidCredentials
    case tokenExpired(refreshToken: String)
    case accountLocked(until: Date)
}

// Обработка
func handleAuthError(_ error: AuthError) {
    switch error {
    case .tokenExpired(let token):
        refreshToken(token)
    case .accountLocked(let date):
        showLockedScreen(until: date)
    default:
        showGenericAlert()
    }
}

3. Комбинирование Result с async/await

Хотя async/await (Swift 5.5+) частично заменил Result в асинхронном коде, Result остаётся полезным для синхронных цепочек и тестирования.

func fetchUserAsync(id: Int) async throws -> User {
    // async реализация
    return User()
}

// Для тестов удобно использовать Result
func testFetchUser() {
    let result = Result { try await fetchUserAsync(id: 1) }
    // ...
}

Заключение

Обработка ошибок в Swift — это не просто try/catch. Продвинутые техники, такие как Result, rethrows и кастомные иерархии ошибок, позволяют писать более надёжный, читаемый и поддерживаемый код. Используйте эти инструменты в своих iOS/macOS проектах, и ваши приложения станут устойчивее к сбоям, а код — чище.

Помните: хорошая обработка ошибок — это не защита от всех возможных проблем, а грамотное управление непредвиденными ситуациями. Проектируйте свои ошибки так же тщательно, как и бизнес-

Блоги

Рекомендаций по книгам