Обработка ошибок в 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 проектах, и ваши приложения станут устойчивее к сбоям, а код — чище.
Помните: хорошая обработка ошибок — это не защита от всех возможных проблем, а грамотное управление непредвиденными ситуациями. Проектируйте свои ошибки так же тщательно, как и бизнес-