Перейти к содержимому
Теория · Ошибки · урок 1 из 7

Ошибки и throw

Чему вы научитесь

Чему вы научитесь:

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

Представьте почту, где посылки едут по ленте от окошка к окошку. В первом окошке посылку принимают, а в последнем читают адрес и отправляют по нему. Но иногда адрес не разобрать. Сотрудник не гадает и не бросает посылку в угол: он ставит на неё штамп «вернуть отправителю», и лента везёт её назад. У первого окошка натянута сетка, в неё посылка и падает.

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

Зачем это нужно, если есть опционалы? Из темы об опционалах вы знаете, что Int("42") даёт Int?, а для текста не из цифр — nil. Но nil не объясняет, что именно не так:

SWIFT
let empty = Int("")
let letters = Int("4x2")
print(empty == nil, letters == nil) // true true

Оба раза получился nil. А беды разные: в первом случае текст пустой, во втором в нём есть буква. Опционал — коробка, которая бывает пустой, но записку с причиной в неё не положишь. Чтобы сообщить причину, в Swift есть ошибки.

Тип ошибки и throw

Ошибка в Swift — значение типа, который соответствует протоколу Error. Чаще всего такой тип — перечисление: каждый его вариант называет одну причину сбоя. В отличие от ошибки компиляции, такая ошибка возникает, когда программа уже работает. Напишем функцию parse, которая читает число из текста и сообщает, почему не смогла:

SWIFT
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
}

Давайте разберём программу по строкам:

  1. enum ParseError: Error объявляет перечисление с двумя причинами: empty — текст пустой, badFormat — в тексте не число. : Error после имени — соответствие протоколу Error.
  2. Слово throws после параметров говорит, что функция может выбросить ошибку. Оно стоит перед стрелкой ->.
  3. throw ParseError.empty выбрасывает ошибку, если текст пустой.
  4. throw выходит из функции, как return, поэтому годится и для else у guard.
  5. Если обе проверки пройдены, return n возвращает число, как обычно.

Выбросить ошибку значит сразу завершить функцию и вместо результата отдать ошибку тому, кто функцию вызвал. Строки ниже throw уже не выполняются. Если убрать throws из заголовка parse, строки с throw не скомпилируются. Компилятор требует предупредить, что функция может вернуть ошибку.

Этот вопрос часто задают на собеседовании: что такое Error? Ответ:

  • Error — пустой протокол: в нём нет ни требований-свойств, ни требований-методов, поэтому перечислению хватает : Error после имени;
  • соответствие Error разрешает выбросить значение через throw;
  • ошибки обычно описывают перечислением, а его варианты называют причины сбоя.

Поймать ошибку: do, try, catch

Функцию с throws вызывают иначе, чем обычную. Перед вызовом пишут try, а сам вызов кладут в блок do, за которым идёт catch:

SWIFT
do {
    let n = try parse("42")
    print(n + 1) // 43
    print(try parse("4x2"))
    print("done")
} catch {
    print("error:", error) // error: badFormat
}

Давайте пройдём по программе:

  1. do { … } — блок, внутри которого может прилететь ошибка.
  2. try отмечает вызов, который может выбросить ошибку. Его пишут и внутри выражения: print(try parse("4x2")).
  3. parse("42") возвращает 42, и программа печатает 43.
  4. parse("4x2") выбрасывает ParseError.badFormat. Остаток блока do пропускается: не выполняется ни print с этим вызовом, ни print("done").
  5. Выполнение переходит в catch. Внутри него пойманная ошибка лежит в константе error, и print показывает её вариант — badFormat.
  6. После catch программа идёт к строкам под ним, а назад в do не возвращается.

Поймать ошибку значит принять её в catch и решить, что делать: напечатать сообщение, подставить значение или попросить ввести текст заново. Выброшенную ошибку ловит ближайший catch на её обратном пути — здесь тот, что стоит сразу за do. Если ошибки не было, catch не выполняется вовсе.

Здесь картинка с почтой неточна. Сотрудник может отложить трудную посылку и вернуться к ней позже. Функция после throw к работе не возвращается: строки под throw не выполнятся, а вызов не вернёт никакого значения.

Частая ошибка

Функцию с throws вызвали без try:

SWIFT
do {
    let k = parse("7") // ✗
    print(k)
} catch {
    print(error)
}
TEXT
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 после имени.