Ошибки и throw
Чему вы научитесь:
- объявлять тип ошибки, выбрасывать ошибку через
throwи ловить её вdo/catch; - пробрасывать ошибку через несколько функций и ловить отдельные её варианты;
- превращать ошибку в
nilчерезtry?и объяснять, когдаtry!аварийно завершает программу; - убирать за собой при любом выходе из функции с помощью
defer.

Представьте почту, где посылки едут по ленте от окошка к окошку. В первом окошке посылку принимают, а в последнем читают адрес и отправляют по нему. Но иногда адрес не разобрать. Сотрудник не гадает и не бросает посылку в угол: он ставит на неё штамп «вернуть отправителю», и лента везёт её назад. У первого окошка натянута сетка, в неё посылка и падает.
По штампу отправитель сразу видит, что пошло не так, и решает, что делать дальше. Функции в программе умеют то же самое. Функция, которая не смогла выполнить работу, отдаёт не результат, а ошибку. Ошибка едет назад, к тому, кто вызвал функцию, пока её не поймают.
Зачем это нужно, если есть опционалы? Из темы об опционалах вы знаете, что Int("42") даёт Int?, а для текста не из цифр — nil. Но nil не объясняет, что именно не так:
let empty = Int("")
let letters = Int("4x2")
print(empty == nil, letters == nil) // true trueОба раза получился nil. А беды разные: в первом случае текст пустой, во втором в нём есть буква. Опционал — коробка, которая бывает пустой, но записку с причиной в неё не положишь. Чтобы сообщить причину, в Swift есть ошибки.
Тип ошибки и throw
Ошибка в Swift — значение типа, который соответствует протоколу Error. Чаще всего такой тип — перечисление: каждый его вариант называет одну причину сбоя. В отличие от ошибки компиляции, такая ошибка возникает, когда программа уже работает. Напишем функцию parse, которая читает число из текста и сообщает, почему не смогла:
enum ParseError: Error {
case empty
case badFormat
}
func parse(_ text: String) throws -> Int {
if text.isEmpty {
throw ParseError.empty
}
guard let n = Int(text) else {
throw ParseError.badFormat
}
return n
}Давайте разберём программу по строкам:
enum ParseError: Errorобъявляет перечисление с двумя причинами:empty— текст пустой,badFormat— в тексте не число.: Errorпосле имени — соответствие протоколуError.- Слово
throwsпосле параметров говорит, что функция может выбросить ошибку. Оно стоит перед стрелкой->. throw ParseError.emptyвыбрасывает ошибку, если текст пустой.throwвыходит из функции, какreturn, поэтому годится и дляelseуguard.- Если обе проверки пройдены,
return nвозвращает число, как обычно.
Выбросить ошибку значит сразу завершить функцию и вместо результата отдать ошибку тому, кто функцию вызвал. Строки ниже throw уже не выполняются. Если убрать throws из заголовка parse, строки с throw не скомпилируются. Компилятор требует предупредить, что функция может вернуть ошибку.
Этот вопрос часто задают на собеседовании: что такое Error? Ответ:
Error— пустой протокол: в нём нет ни требований-свойств, ни требований-методов, поэтому перечислению хватает: Errorпосле имени;- соответствие
Errorразрешает выбросить значение черезthrow; - ошибки обычно описывают перечислением, а его варианты называют причины сбоя.
Поймать ошибку: do, try, catch
Функцию с throws вызывают иначе, чем обычную. Перед вызовом пишут try, а сам вызов кладут в блок do, за которым идёт catch:
do {
let n = try parse("42")
print(n + 1) // 43
print(try parse("4x2"))
print("done")
} catch {
print("error:", error) // error: badFormat
}Давайте пройдём по программе:
do { … }— блок, внутри которого может прилететь ошибка.tryотмечает вызов, который может выбросить ошибку. Его пишут и внутри выражения:print(try parse("4x2")).parse("42")возвращает42, и программа печатает43.parse("4x2")выбрасываетParseError.badFormat. Остаток блокаdoпропускается: не выполняется ниprintс этим вызовом, ниprint("done").- Выполнение переходит в
catch. Внутри него пойманная ошибка лежит в константеerror, иprintпоказывает её вариант —badFormat. - После
catchпрограмма идёт к строкам под ним, а назад вdoне возвращается.
Поймать ошибку значит принять её в catch и решить, что делать: напечатать сообщение, подставить значение или попросить ввести текст заново. Выброшенную ошибку ловит ближайший catch на её обратном пути — здесь тот, что стоит сразу за do. Если ошибки не было, catch не выполняется вовсе.
Здесь картинка с почтой неточна. Сотрудник может отложить трудную посылку и вернуться к ней позже. Функция после throw к работе не возвращается: строки под throw не выполнятся, а вызов не вернёт никакого значения.
Частая ошибка
Функцию с throws вызвали без try:
do {
let k = parse("7") // ✗
print(k)
} catch {
print(error)
}error: call can throw but is not marked with 'try'Программа не компилируется. Сообщение говорит: вызов может выбросить ошибку, но не отмечен try. Компилятор требует try у каждого такого вызова, чтобы по коду было видно, откуда программа может свернуть в catch. Исправление — let k = try parse("7").
Главное
- Ошибка — значение типа, который соответствует протоколу
Error; обычно это перечисление с причинами сбоя. - Функция с
throwsвыбрасывает ошибку черезthrowи при этом сразу завершается без результата. - Функцию с
throwsвызывают сtryвнутриdo: при ошибке остатокdoпропускается и выполняетсяcatch, где ошибка лежит в константеerror. Error— пустой протокол: перечислению для соответствия хватает: Errorпосле имени.