catch и проброс ошибки
В первом уроке parse вызывали прямо из блока do. В настоящей программе между ними часто стоит ещё одна функция.
Вернёмся на почту: на ленте три окошка. В первом посылку принимают, и у него натянута сетка catch. Во втором считают цену пересылки. В третьем, parse, читают вес, написанный на посылке.
Если вес не разобрать, parse ставит штамп throw, и посылка едет назад. Второе окошко её не ловит, а пропускает дальше, и падает она в сетку первого. Начнём с перечисления и функции parse, которые уже встречались:
enum ParseError: Error {
case empty, badFormat
}
func parse(_ text: String) throws -> Int {
if text.isEmpty {
throw ParseError.empty
}
guard let n = Int(text) else {
throw ParseError.badFormat
}
return n
}У parse три исхода:
- пустой текст — ошибка
empty; - текст не из цифр — ошибка
badFormat; - текст с числом — само число.
Ошибка проходит через функцию
Добавим второе окошко — функцию cost. Она получает вес текстом, читает его через parse и считает цену: три монеты за килограмм.
func cost(_ weight: String) throws -> Int {
let kg = try parse(weight)
return kg * 3
}
do {
print(try cost("4")) // 12
print(try cost(""))
} catch {
print("error:", error) // error: empty
}У cost нет своего do/catch. Ошибку из parse она не ловит, а отдаёт дальше — тому, кто вызвал саму cost. Это называют пробросом ошибки. Для проброса нужны две вещи:
tryперед вызовомparse— место, откуда может прилететь ошибка;throwsв заголовкеcost— предупреждение для тех, кто её вызывает: из этой функции тоже может выйти ошибка.
Нажимайте «Следующий шаг» и следите за столбиком рамок: на втором вызове ошибка снимает рамки parse и cost, и ни одна из них не возвращает число.
1enum ParseError: Error {2 case empty, badFormat3}4 5func parse(_ text: String) throws -> Int {6 if text.isEmpty {7 throw ParseError.empty8 }9 guard let n = Int(text) else {10 throw ParseError.badFormat11 }12 return n13}14 15func cost(_ weight: String) throws -> Int {16 let kg = try parse(weight)17 return kg * 318}19 20do {21 print(try cost("4"))22 print(try cost(""))23} catch {24 print("error:", error)25}
Программа вызывает cost с текстом 4 и ждёт цену.
Что видно в виджете:
throwзавершаетparseсразу, безreturn;- в
costошибка выходит на строке сtry, иreturn kg * 3уже не выполняется; - ошибку ловит ближайший
catchна обратном пути, сколько бы функций ни стояло между.
Этот вопрос часто задают на собеседовании: как ошибка проходит через несколько функций? Каждая функция на пути ошибки делает одно из двух:
- ловит ошибку своим
do/catch; - пробрасывает её дальше: тогда функция помечена
throws, а вызов внутри неё —try.
Ловить отдельные варианты
Сейчас catch ловит любую ошибку. Но на разные причины часто отвечают по-разному: на пустой текст просят ввести вес, а на буквы говорят, что это не число. Для этого у do бывает несколько веток catch:
do {
print(try cost("4x"))
} catch ParseError.empty {
print("no weight")
} catch {
print("other:", error) // other: badFormat
}Ветки catch проверяются сверху вниз, как ветки switch, и выполняется первая подходящая.
Ветка для одного варианта
catch ParseError.empty ловит только этот вариант ошибки. Здесь cost("4x") выбросила badFormat, поэтому ветка не подошла, и no weight не напечаталось.
Ветка для остальных
catch без шаблона ловит всё, что не поймали ветки выше. Внутри него пойманная ошибка лежит в константе error, и print показывает её вариант. Такую ветку обычно ставят последней.
Если ошибку никто не поймал
В главной части программы, то есть в строках main.swift вне функций, вызов с try можно написать и без do: print(try cost("")). Программа скомпилируется, но поймать ошибку будет некому, и при запуске она аварийно завершится:
Fatal error: Error raised at top level: main.ParseError.emptyСообщение говорит: ошибка дошла до главной части программы. В конце названа сама ошибка — тип ParseError и вариант empty. main перед ними — имя программы: команда swiftc -swift-version 6 main.swift собирает программу main. Если программа называется иначе, например в Xcode, на месте main стоит её имя.
Здесь картинка с почтой неточна. Посылку, которую никто не забрал, почта положит на склад и подождёт. А ошибка, которую никто не поймал, останавливает всю программу.
Частая ошибка
Функция вызывает parse через try, но throws в её заголовке забыли:
func price(_ weight: String) -> Int { // ✗
let kg = try parse(weight)
return kg * 3
}error: errors thrown from here are not handledПрограмма не компилируется. Сообщение говорит: ошибку, которая может прилететь отсюда, никто не обрабатывает. У функции без throws есть два выхода:
- добавить
throwsв заголовок и пробросить ошибку дальше; - поймать ошибку внутри функции своим
do/catchи закончить его веткойcatchбез шаблона.
Главное
- Функция с
throwsможет не ловить ошибку, а пробросить её: вызов внутри неё пишут сtry, и ошибка уходит к вызывающему. - Ошибка снимает все вызовы на своём пути, пока её не поймает ближайший
catch; строки нижеtryне выполняются. catch ParseError.emptyловит один вариант, аcatchбез шаблона — все остальные, с ошибкой в константеerror.- Ошибка, которую никто не поймал, аварийно завершает программу:
Fatal error: Error raised at top level.