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

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

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

Начать курс

Обработка ошибок в 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 приложения, которые корректно обрабатывают любые непредвиденные ситуации.

Блоги

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