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

catch и проброс ошибки

В первом уроке parse вызывали прямо из блока do. В настоящей программе между ними часто стоит ещё одна функция.

Вернёмся на почту: на ленте три окошка. В первом посылку принимают, и у него натянута сетка catch. Во втором считают цену пересылки. В третьем, parse, читают вес, написанный на посылке.

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

SWIFT
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 и считает цену: три монеты за килограмм.

SWIFT
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, и ни одна из них не возвращает число.

ВживуюОшибка выходит из двух вызововшаг 1 из 10 · рамок: 1

Программа вызывает cost с текстом 4 и ждёт цену.

Следите за рамками: ошибка снимает parse и cost, и ни одна не возвращает число

Что видно в виджете:

  • throw завершает parse сразу, без return;
  • в cost ошибка выходит на строке с try, и return kg * 3 уже не выполняется;
  • ошибку ловит ближайший catch на обратном пути, сколько бы функций ни стояло между.

Этот вопрос часто задают на собеседовании: как ошибка проходит через несколько функций? Каждая функция на пути ошибки делает одно из двух:

  • ловит ошибку своим do/catch;
  • пробрасывает её дальше: тогда функция помечена throws, а вызов внутри неё — try.

Ловить отдельные варианты

Сейчас catch ловит любую ошибку. Но на разные причины часто отвечают по-разному: на пустой текст просят ввести вес, а на буквы говорят, что это не число. Для этого у do бывает несколько веток catch:

SWIFT
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("")). Программа скомпилируется, но поймать ошибку будет некому, и при запуске она аварийно завершится:

TEXT
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 в её заголовке забыли:

SWIFT
func price(_ weight: String) -> Int { // ✗
    let kg = try parse(weight)
    return kg * 3
}
TEXT
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.