Обработка ошибок в Swift: try/catch и не только — продвинутый уровень
Обработка ошибок — неотъемлемая часть любой серьёзной iOS/macOS разработки. Swift предоставляет мощную и выразительную систему управления ошибками, которая выходит далеко за рамки простого try/catch. В этой статье мы рассмотрим продвинутые техники: работу с протоколом Error, перечисления ошибок, ключевые слова throws, rethrows, а также тип Result и функциональные подходы. Материал рассчитан на разработчиков, уже знакомых с основами Swift.
1. Протокол Error и пользовательские типы ошибок
В основе обработки ошибок в Swift лежит протокол Error. Любой тип, соответствующий этому протоколу, может быть выброшен как ошибка. Чаще всего для этого используют перечисления (enum).
import Foundation
// Определяем пользовательский тип ошибки для сетевого запроса
enum NetworkError: Error {
case invalidURL
case noData
case decodingFailed(description: String)
case serverError(statusCode: Int)
}
// Функция, которая может выбросить ошибку
func fetchUserData(from urlString: String) throws -> Data {
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL
}
// Симуляция сетевого запроса
let success = Bool.random()
if !success {
throw NetworkError.serverError(statusCode: 500)
}
return Data()
}
Использование перечислений позволяет группировать связанные ошибки и передавать дополнительную информацию (associated values), что делает код более читаемым и самодокументируемым.
2. Продвинутое использование do-catch
Блок do-catch — основной инструмент для перехвата ошибок. На продвинутом уровне важно уметь обрабатывать разные типы ошибок по-разному.
func processUserData() {
do {
let data = try fetchUserData(from: "https://api.example.com/user")
// Работаем с данными
print("Данные получены: \(data.count) байт")
} catch NetworkError.invalidURL {
print("Ошибка: неверный URL")
} catch NetworkError.serverError(let code) where code >= 500 {
print("Серверная ошибка с кодом \(code). Повторите попытку позже.")
} catch let error as NetworkError {
print("Сетевая ошибка: \(error)")
} catch {
// Обработка всех остальных ошибок
print("Неизвестная ошибка: \(error.localizedDescription)")
}
}
Ключевые моменты:
- Паттерн-матчинг с
where: позволяет фильтровать ошибки по дополнительным условиям (например, диапазон кодов ответа). - Приведение типа (
as): если вы ожидаете несколько типов ошибок, можно использоватьcatch let error as MyErrorType. - Универсальный
catch: всегда должен быть последним, так как перехватывает любую необработанную ошибку.
3. Ключевые слова throws, rethrows и propagating errors
Ключевое слово throws указывает, что функция может выбросить ошибку. rethrows — более тонкий механизм: он говорит, что функция может выбросить ошибку только если один из её замыкающих параметров (closure) выбросил ошибку.
// Функция с throws — сама может выбросить ошибку
func loadConfig() throws -> [String: String] {
// ...
throw NetworkError.noData
}
// Функция с rethrows — передаёт ошибку из замыкания
func performWithRetry(times: Int, operation: () throws -> Void) rethrows {
for _ in 0..<times {
try operation()
}
}
// Использование
do {
try performWithRetry(times: 3) {
try loadConfig()
}
} catch {
print("Операция не удалась после 3 попыток: \(error)")
}
Преимущество rethrows в том, что если вы передаёте не выбрасывающее замыкание, то и саму функцию не нужно вызывать с try. Это делает API более гибким.
4. Тип Result и функциональная обработка ошибок
Тип Result<Success, Failure> — это современный способ обработки ошибок без использования try/catch. Он особенно полезен в асинхронном коде и при цепочках операций.
// Функция, возвращающая Result
func fetchDataResult(from urlString: String) -> Result<Data, NetworkError> {
guard let url = URL(string: urlString) else {
return .failure(.invalidURL)
}
let success = Bool.random()
if success {
return .success(Data())
} else {
return .failure(.serverError(statusCode: 404))
}
}
// Обработка с помощью switch
let result = fetchDataResult(from: "https://example.com")
switch result {
case .success(let data):
print("Успех: \(data)")
case .failure(let error):
print("Провал: \(error)")
}
// Комбинирование с map и flatMap
let processedResult = result
.map { data in
// Преобразуем данные
return String(data: data, encoding: .utf8) ?? "Нет данных"
}
.mapError { error in
// Преобразуем ошибку
return "Ошибка сети: \(error.localizedDescription)"
}
if let value = try? processedResult.get() {
print("Результат: \(value)")
}
Методы map, flatMap, mapError позволяют строить цепочки обработки без вложенных if-let или do-catch. Это делает код более декларативным и безопасным.
Заключение
Обработка ошибок в Swift — это не просто try/catch. Использование пользовательских перечислений с Error, паттерн-матчинга в catch, ключевого слова rethrows и типа Result позволяет писать надёжный, читаемый и масштабируемый код. Выбор подхода зависит от контекста: для синхронных операций хорош do-catch, для асинхронных и функциональных цепочек — Result. Освоив эти техники, вы сможете создавать более устойчивые iOS/macOS приложения, которые корректно обрабатывают любые непредвиденные ситуации.