Как 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, включая производительность, память и сопровождение.