SwiftMiddleTechnical

Как Swift обрабатывает ошибки с помощью throws, try, catch и Result<T, E>?

throws/try/catch — синхронный механизм обработки ошибок в Swift с проверкой компилятором. Result<T, E> — тип для отложенной или асинхронной передачи результата. Оба подхода типобезопасны и взаимоконвертируемы.

Два механизма обработки ошибок

Swift предоставляет два complementary подхода: throws/try/catch для синхронного и async кода, и Result<T, E> — value-тип для хранения и передачи результата. Они решают разные задачи и легко конвертируются друг в друга.

throws, try, catch — базовый механизм

Функция, помеченная throws, может выбросить ошибку. Вызывающий код обязан обработать её через do-catch. Компилятор не позволит проигнорировать ошибку — в отличие от Objective-C NSError.

// Определение ошибок
enum ValidationError: Error {
    case emptyField(name: String)
    case invalidFormat(field: String, expected: String)
    case outOfRange(value: Int, min: Int, max: Int)
}

enum NetworkError: Error {
    case noConnection
    case timeout(seconds: Double)
    case httpError(statusCode: Int)
}

// Функция, которая бросает ошибку
func validateAge(_ input: String) throws -> Int {
    guard !input.isEmpty else {
        throw ValidationError.emptyField(name: "age")
    }
    guard let age = Int(input) else {
        throw ValidationError.invalidFormat(field: "age", expected: "integer")
    }
    guard (0...150).contains(age) else {
        throw ValidationError.outOfRange(value: age, min: 0, max: 150)
    }
    return age
}

// Обработка ошибок
do {
    let age = try validateAge("25")
    print("Valid age: \(age)")
} catch ValidationError.emptyField(let name) {
    print("Field '\(name)' is required")
} catch ValidationError.invalidFormat(let field, let expected) {
    print("Field '\(field)' must be \(expected)")
} catch ValidationError.outOfRange(let value, let min, let max) {
    print("\(value) must be between \(min) and \(max)")
} catch {
    // Catch-all: error — неявная переменная типа Error
    print("Unexpected error: \(error)")
}

try?, try! и rethrows

// try? — конвертирует ошибку в nil, возвращает Optional
let age = try? validateAge("abc")  // nil
let validAge = try? validateAge("30")  // Optional(30)

// try! — crash при ошибке, используйте только если уверены на 100%
let knownGoodAge = try! validateAge("25")  // небезопасно

// rethrows — функция бросает только если переданное замыкание бросает
func transform<T, U>(_ value: T, using closure: (T) throws -> U) rethrows -> U {
    try closure(value)
}
// Если замыкание не throws — вызов transform тоже не requires try
let doubled = transform(5) { $0 * 2 }  // не нужен try
let parsed = try transform("42") { s throws in
    guard let n = Int(s) else { throw ValidationError.invalidFormat(field: "value", expected: "Int") }
    return n
}

Result<T, E> — для асинхронных и отложенных сценариев

Result — enum с двумя кейсами: .success(T) и .failure(E). Удобен когда результат нужно сохранить, передать через callback или обработать позже.

// Функция возвращает Result вместо throws
func fetchUser(id: String, completion: @escaping (Result<User, NetworkError>) -> Void) {
    URLSession.shared.dataTask(with: userURL(id)) { data, response, error in
        if let error = error as? URLError {
            completion(.failure(.noConnection))
            return
        }
        guard let data = data,
              let user = try? JSONDecoder().decode(User.self, from: data) else {
            completion(.failure(.httpError(statusCode: 422)))
            return
        }
        completion(.success(user))
    }.resume()
}

// Обработка
fetchUser(id: "u42") { result in
    switch result {
    case .success(let user):
        DispatchQueue.main.async { self.updateUI(with: user) }
    case .failure(.noConnection):
        DispatchQueue.main.async { self.showOfflineAlert() }
    case .failure(let error):
        print("Error: \(error)")
    }
}

// Удобные методы Result
let result: Result<Int, ValidationError> = .success(42)
let value = result.get()  // throws если .failure
let mapped = result.map { $0 * 2 }           // Result<Int, ValidationError>
let flatMapped = result.flatMap { v -> Result<String, ValidationError> in
    .success("\(v)")
}

Конвертация между throws и Result

// throws -> Result
let result = Result { try validateAge("25") }  // Result<Int, Error>

// Result -> throws (в async контексте)
async func processUser(id: String) async throws -> User {
    let result = await fetchUserAsync(id: id)  // Result<User, NetworkError>
    return try result.get()  // throws NetworkError если .failure
}

// Современный async/await с throws — предпочтительный подход
func fetchUserModern(id: String) async throws -> User {
    let (data, response) = try await URLSession.shared.data(from: userURL(id))
    guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
        throw NetworkError.httpError(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0)
    }
    return try JSONDecoder().decode(User.self, from: data)
}

// Вызов
do {
    let user = try await fetchUserModern(id: "u42")
    updateUI(with: user)
} catch NetworkError.noConnection {
    showOfflineAlert()
} catch {
    showGenericError(error)
}

Подводные камни

  • try без do-catch не скомпилируется — если не использовать try? или try!. Нельзя вызвать throwing функцию и «забыть» про ошибку. Это намеренное ограничение Swift в отличие от Java checked exceptions, где ошибку можно пробросить молча.
  • catch-all без typed catch теряет информацию. catch { print(error) } работает, но вы теряете возможность типизированно обработать ошибку. Всегда старайтесь сначала перечислить конкретные кейсы, catch-all — последний resort.
  • Result<T, Error> vs Result<T, MyError>. Использование протокола Error вместо конкретного типа удобно, но лишает exhaustive matching. Если используете конкретный тип, компилятор гарантирует, что все кейсы покрыты.
  • try? скрывает ошибку навсегда. Если что-то пошло не так, вы получите nil без какой-либо информации о причине. Используйте осознанно только когда ошибка действительно не важна.
  • Ошибки в async контексте требуют async throws. Функция может быть одновременно async throws. Вызов — try await. Забыть одно из ключевых слов — ошибка компилятора.
  • Error не несёт структурированной информации по умолчанию. Голый Error без ассоциированных значений бесполезен для пользователя. Всегда делайте ошибки информативными: код, описание, контекст.
  • Не злоупотребляйте throws для control flow. Бросать ошибку при «пустом результате» — антипаттерн. Для ожидаемых «нет данных» используйте Optional или Result с кейсом-не-ошибкой. throws предназначен для исключительных ситуаций.

Common mistakes

  • Сводить «Swift обрабатывает ошибки с помощью throws, try, catch и Result<T, E>» к синтаксису и не объяснять Swift Concurrency.
  • Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swift-12.
  • Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.

What the interviewer is testing

  • Формулирует точную модель для «Swift обрабатывает ошибки с помощью throws, try, catch и Result<T, E>» и подтверждает ее корректным примером.
  • Умеет связать ответ с границы Objective-C, тестированием и отладкой на устройстве.
  • Называет ограничения подхода swift-12, включая производительность, память и сопровождение.

Sources

Related topics

Как Swift обрабатывает ошибки с помощью `throws`, `try`, `catch` и `Result<T, E>`? | Talanto