Обработка ошибок в Swift: try/catch и не только: от А до Я

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

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

Начать курс

Обработка ошибок в Swift: try/catch и не только — полное руководство

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

Основы: протокол Error и ключевое слово throws

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

enum PasswordError: Error {
    case tooShort
    case missingDigit
    case missingUppercase
}

func validatePassword(_ password: String) throws {
    guard password.count >= 8 else {
        throw PasswordError.tooShort
    }
    guard password.rangeOfCharacter(from: .decimalDigits) != nil else {
        throw PasswordError.missingDigit
    }
    guard password.rangeOfCharacter(from: .uppercaseLetters) != nil else {
        throw PasswordError.missingUppercase
    }
    print("Пароль корректен")
}

Ключевое слово throws в объявлении функции сообщает компилятору, что выполнение может быть прервано ошибкой. Вызов такой функции требует специального синтаксиса.

Конструкция do-catch и ключевое слово try

Для обработки потенциальных ошибок используется блок do-catch с пометкой try перед вызовом:

do {
    try validatePassword("Hello123")
    print("Успех!")
} catch PasswordError.tooShort {
    print("Ошибка: пароль слишком короткий")
} catch PasswordError.missingDigit {
    print("Ошибка: в пароле нет цифры")
} catch PasswordError.missingUppercase {
    print("Ошибка: в пароле нет заглавной буквы")
} catch {
    print("Неизвестная ошибка: \(error)")
}

Обратите внимание на последний блок catch — он перехватывает любые неучтённые ранее ошибки. Это обязательное требование: компилятор Swift проверяет, что все возможные ошибки обработаны.

try? и try! — упрощённые варианты обработки

Иногда вам не нужен полный контроль над ошибками. Swift предлагает два сокращённых варианта:

  • try? — преобразует ошибку в опциональное значение nil. Если ошибки нет — возвращает результат, обёрнутый в Optional.
  • try! — утверждает, что ошибки не будет. Если она всё же возникнет, приложение аварийно завершится (runtime crash).
// try? — безопасное игнорирование ошибки
if let _ = try? validatePassword("weak") {
    print("Пароль прошёл проверку")
} else {
    print("Пароль не прошёл")
}

// try! — только если вы уверены на 100%
let result = try! validatePassword("StrongPass1") // Краш, если пароль невалиден

Используйте try! только в ситуациях, где ошибка теоретически невозможна (например, при загрузке ресурса, встроенного в бандл приложения).

Продвинутые техники: rethrows, defer и Result

Переброс ошибок (rethrows)

Ключевое слово rethrows применяется к функциям высшего порядка, которые принимают замыкание, способное выбрасывать ошибку. Функция сама не выбрасывает ошибку, но передаёт её из замыкания:

func processItems(_ items: [String], using validator: (String) throws -> Void) rethrows {
    for item in items {
        try validator(item)
    }
}

// Использование
do {
    try processItems(["Hello", "World123"], using: validatePassword)
} catch {
    print("Ошибка обработки: \(error)")
}

Это позволяет писать гибкие функции-обёртки без дублирования кода обработки ошибок.

Очистка ресурсов с defer

Блок defer гарантирует выполнение кода при любом завершении функции — как при успехе, так и при ошибке. Идеально подходит для закрытия файлов, освобождения памяти или завершения сетевых сессий:

func readFile(at path: String) throws -> String {
    let file = openFile(path) // Гипотетическая функция
    defer {
        closeFile(file) // Закроется в любом случае
        print("Файл закрыт")
    }
    
    guard file.isValid else {
        throw FileError.corrupted
    }
    return file.read()
}

Помните: блоки defer выполняются в обратном порядке их объявления.

Тип Result для асинхронных операций

Начиная с Swift 5, тип Result<Success, Failure> позволяет обрабатывать ошибки без исключений — особенно удобно в асинхронных функциях и замыканиях:

enum NetworkError: Error {
    case badURL
    case noData
}

func fetchUser(completion: (Result<String, NetworkError>) -> Void) {
    let success = Bool.random()
    if success {
        completion(.success("User: John"))
    } else {
        completion(.failure(.noData))
    }
}

// Использование
fetchUser { result in
    switch result {
    case .success(let user):
        print(user)
    case .failure(let error):
        print("Ошибка: \(error)")
    }
}

Result особенно полезен при работе с Combine, async/await или старыми callback-based API.

Лучшие практики и частые ошибки

  • Не злоупотребляйте try! — это источник неожиданных крашей. Используйте только в unit-тестах или когда уверены в данных на 100%.
  • Создавайте свои типы ошибок — используйте перечисления с ассоциированными значениями для передачи дополнительного контекста.
  • Избегайте пустых catch-блоков — всегда логируйте или обрабатывайте ошибку, даже если просто пробрасываете её дальше.
  • Используйте defer для ресурсов — это гарантирует корректную очистку даже при неожиданных ошибках.
  • Для асинхронного кода предпочитайте Result или async throws (в современных версиях Swift) — это делает поток ошибок явным.

Заключение

Обработка ошибок

Блоги

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