JavaRush /Курсы /Swift SELF /do/catch — обработка ошибок

do/catch — обработка ошибок

Swift SELF
29 уровень , 2 лекция
Открыта

1. do/catch: зачем нужен и как устроен

Сначала может показаться, что try — это и есть обработка: поставил try, и всё стало безопасно. Но try — это скорее табличка «Осторожно, тут может быть ошибка», а не спасательная служба. do/catch — это как раз спасательная служба: он говорит Swift, что в этом месте мы готовы принять решение, что делать при неудаче.

Представьте бытовую аналогию. try — это как прочитать на коробке «может содержать следы орехов». А do/catch — это момент, когда вы решаете: «Окей, у меня аллергия — значит, я не ем это, а беру другую коробку». Без решения предупреждение бесполезно.

В Swift логика такая: мы вызываем throwing‑функцию с try, а затем либо продолжаем выполнение (если всё хорошо), либо перепрыгиваем в catch (если была ошибка).

Наглядно это можно представить так:

flowchart TD
    A["do { ... }"] --> B[try вызвать throwing-функцию]
    B -->|успех| C[продолжаем код внутри do]
    B -->|ошибка| D[прыжок в catch]
    D --> E[реакция: сообщение / другой путь / остановка сценария]

Базовый синтаксис do/catch

Когда вы впервые пишете do/catch, хочется сделать его “на всякий случай” и забыть. Но его сила не в том, что он «ловит всё», а в том, что он явно фиксирует точку ответственности: вот здесь мы готовы обработать проблему.

Минимальная форма выглядит так: внутри do пишем код с try, а в catch — реакцию. Важно: в catch у нас есть переменная error (типа Error), чтобы хотя бы что-то вывести и не потерять причину.

import Foundation

enum MathError: Error { case divisionByZero }

func divide(_ a: Int, by b: Int) throws -> Int {
    guard b != 0 else { throw MathError.divisionByZero }
    return a / b
}

do {
    let x = try divide(10, by: 2)
    print("Ответ: \(x)")           // Ответ: 5
} catch {
    print("Ошибка: \(error)")
}

Обратите внимание на психологический эффект: мы перестали надеяться на лучшее и начали писать код так, будто ошибки — это нормальная часть жизни. Это уже шаг к зрелому стилю, даже если вы пока пишете учебные программы.

Несколько catch: порядок важнее, чем кажется

Один catch — это нормально, но часто хочется реагировать по-разному на разные причины. И Swift это позволяет: можно написать несколько catch, которые проверяются сверху вниз, как else if.

Тут есть классическое правило, почти как в жизни: «Сначала разбираемся с понятными и ожидаемыми проблемами, и только в конце — “ну вообще всё остальное”».

import Foundation

enum MathError: Error { case divisionByZero }

func divide(_ a: Int, by b: Int) throws -> Int {
    guard b != 0 else { throw MathError.divisionByZero }
    return a / b
}

do {
    print(try divide(10, by: 0))
} catch MathError.divisionByZero {
    print("Нельзя делить на ноль") // Нельзя делить на ноль
} catch {
    print("Неожиданная ошибка: \(error)")
}

Здесь первый catch ловит конкретный кейс, а второй — «страховка». Если поменять их местами, общий catch перехватит всё, и конкретный станет бесполезным (а иногда компилятор ещё и предупредит, что код недостижим).

Чтобы закрепить идею, вот маленькая таблица:

Что написано Что это означает на практике
catch SomeError.someCase { ... }
Ловим только конкретный кейс конкретной ошибки
catch let e as SomeError { ... }
Ловим любой кейс SomeError, дальше разбираем внутри
catch { ... }
Ловим вообще всё, что долетело сюда

Ветвление по типам ошибок: «это моя ошибка или чужая?»

В больших программах (и даже в учебных CLI) часто встречается ситуация: у нас есть свои ошибки (например, ошибки ввода), и есть «какие-то другие» (которые мы пока не планировали). В таком случае удобно сначала попытаться распознать «свои» ошибки по типу, а остальные отдать в общий обработчик.

Для этого есть знакомый приём: catch let error as SomeError.

import Foundation

enum InputError: Error { case empty }

func requireNonEmpty(_ text: String) throws -> String {
    guard !text.isEmpty else { throw InputError.empty }
    return text
}

do {
    _ = try requireNonEmpty("")
    print("OK")
} catch let e as InputError {
    print("Ошибка ввода: \(e)")    // Ошибка ввода: empty
} catch {
    print("Другая ошибка: \(error)")
}

Почему это полезно? Потому что на уровне «ошибка ввода» мы обычно хотим понятное сообщение пользователю, а на уровне «другая ошибка» — хотя бы не потерять информацию и не сделать вид, что всё хорошо.

Pattern matching в catch: достаём associated values

Сильная сторона do/catch в Swift — в том, что catch умеет pattern matching почти как switch. То есть если ваша ошибка хранит контекст через associated values, вы можете извлечь эти значения прямо в catch.

Это особенно удобно для валидации: «пришло значение X, но ожидали диапазон Y…Z».

import Foundation

enum YearError: Error {
    case notANumber(input: String)
    case outOfRange(value: Int, min: Int, max: Int)
}

func parseYear(_ text: String) throws -> Int {
    guard let value = Int(text) else { throw YearError.notANumber(input: text) }
    guard (1900...2100).contains(value) else {
        throw YearError.outOfRange(value: value, min: 1900, max: 2100)
    }
    return value
}

do {
    print(try parseYear("3024"))
} catch YearError.notANumber(let input) {
    print("Год должен быть числом, а не '\(input)'")
} catch YearError.outOfRange(let value, let min, let max) {
    print("Год \(value) вне диапазона \(min)...\(max)") // Год 3024 вне диапазона 1900...2100
}

Обратите внимание: мы не делали никаких дополнительных switch внутри catch. Мы сразу поймали конкретную причину и сразу же извлекли данные.

Один catch на несколько кейсов

Иногда разные причины ошибки должны приводить к одинаковой реакции. Например, «пустая строка» и «строка из пробелов» — для пользователя это почти одно и то же: «вы ничего не ввели».

В Swift можно написать один catch на несколько паттернов через запятую.

import Foundation

enum InputError: Error { case empty, whitespaceOnly }

func normalizeName(_ text: String) throws -> String {
    if text.isEmpty { throw InputError.empty }
    if text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
        throw InputError.whitespaceOnly
    }
    return text
}

do {
    _ = try normalizeName("   ")
} catch InputError.empty, InputError.whitespaceOnly {
    print("Имя не должно быть пустым") // Имя не должно быть пустым
}

Это выглядит компактно и читаемо: вы показываете, что причины разные, но политика реакции — одинаковая.

2. Пример: усиливаем консольный “мини‑LibraryCLI”

Сценарий: добавление книги с понятными сообщениями

Сейчас сделаем важный шаг от «абстрактных примеров» к ощущению, что do/catch реально делает программу удобнее. Мы напишем маленький сценарий: добавление книги в память. Никаких сложных парсеров команд, только простой диалог: запросили название, запросили год, попытались создать книгу.

Сначала опишем модель и ошибки. Ошибки разделим на две группы: ошибки ввода строки и ошибки года. Это позволит красиво ветвиться в catch.

import Foundation

struct Book {
    let title: String
    let year: Int
}

enum TitleError: Error { case empty }
enum YearError: Error { case notANumber(input: String), outOfRange(Int) }

Теперь напишем функции‑валидаторы. Заметьте стиль: каждая функция либо возвращает корректное значение, либо бросает ошибку. Никаких “пустых строк как валидных данных”.

import Foundation

func parseTitle(_ text: String) throws -> String {
    let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !trimmed.isEmpty else { throw TitleError.empty }
    return trimmed
}

func parseYear(_ text: String) throws -> Int {
    guard let y = Int(text) else { throw YearError.notANumber(input: text) }
    guard (1450...2100).contains(y) else { throw YearError.outOfRange(y) }
    return y
}

А теперь самое интересное: точка, где мы реагируем. Мы попробуем создать Book, и если что-то пошло не так — покажем человеку нормальное сообщение и не уроним программу.

import Foundation

func readLineOrEmpty() -> String { readLine() ?? "" }

var books: [Book] = []

print("Название книги:")
let titleText = readLineOrEmpty()

print("Год издания:")
let yearText = readLineOrEmpty()

do {
    let title = try parseTitle(titleText)
    let year = try parseYear(yearText)
    books.append(Book(title: title, year: year))
    print("Добавлено: \(title) (\(year))")
} catch TitleError.empty {
    print("Ошибка: название не должно быть пустым")
} catch YearError.notANumber(let input) {
    print("Ошибка: год '\(input)' не является числом")
} catch YearError.outOfRange(let value) {
    print("Ошибка: год \(value) вне допустимого диапазона")
} catch {
    print("Неожиданная ошибка: \(error)")
}

Здесь видно главное: do/catch превращает «сломалось» в «мы предусмотрели». С точки зрения пользователя это разница между «программа упала» и «программа объяснила, что не так».

do/catch внутри цикла: ошибка как конец попытки, а не конец программы

В реальных консольных сценариях вы редко хотите, чтобы ошибка ввода завершала программу. Обычно ошибка означает: «эта попытка не удалась, давай попробуем ещё раз». И do/catch идеально ложится на цикл: каждая итерация — это попытка, а catch — реакция, после которой можно продолжать.

Сделаем мини‑цикл: пользователь вводит книги, пока не напишет exit. Обратите внимание: мы обрабатываем ошибки локально и продолжаем цикл, не «пробрасывая» их куда-то дальше.

import Foundation

var books: [Book] = []

while true {
    print("Введите title,year или exit:")
    let line = readLine() ?? ""

    if line == "exit" { break }

    do {
        let parts = line.split(separator: ",", maxSplits: 1).map(String.init)
        let title = try parseTitle(parts.first ?? "")
        let year = try parseYear(parts.count > 1 ? parts[1] : "")
        books.append(Book(title: title, year: year))
        print("OK, книг: \(books.count)") // например: OK, книг: 1
    } catch {
        print("Не удалось добавить книгу: \(error)")
    }
}

Да, вывод в catch пока грубоватый: он печатает error как есть. Но это уже огромный прогресс по сравнению с падением программы. А «красивые сообщения» мы как раз делали выше через конкретные catch.

3. Типичные ошибки при использовании do/catch

Ошибка №1: общий catch ставят первым, а конкретные — ниже.
Такой код выглядит логично на глаз («сначала обработаем всё, а потом частные случаи»), но работает наоборот: первый общий catch перехватывает любые ошибки, и специфические ветки становятся недостижимыми. Держите в голове правило: сверху — самое конкретное, снизу — самый общий «пылесос».

Ошибка №2: ловят ошибку и «молчат» — пустой catch { }.
Иногда хочется «чтобы компилировалось» и просто написать пустой catch. Это почти всегда превращается в загадочные баги: что-то не происходит, но почему — непонятно. Если уж ловите ошибку, дайте хотя бы минимальную реакцию: сообщение пользователю, лог в консоль, прекращение сценария.

Ошибка №3: в catch печатают одно и то же «что-то пошло не так» для всех причин.
Технически это обработка, но по смыслу — потеря информации. Если вы уже проектировали типизированные ошибки с кейсами и контекстом, используйте это богатство: делайте разные catch или pattern matching по associated values, чтобы сообщения были конкретными.

Ошибка №4: пытаются «ветвиться по строке ошибки», а не по типу/кейсу.
Новички иногда делают сравнения вроде "\(error)".contains("empty"). Это хрупко и нечестно: строка — не контракт. Контракт — это тип ошибки и её кейс. В Swift у вас для этого есть catch SomeError.someCase и catch let e as SomeError, и это намного надёжнее.

Ошибка №5: пишут огромный do на полфайла.
do/catch хорошо читается, когда в do лежит один понятный сценарий: «попробовать сделать X». Если вы запихнули туда 200 строк, то непонятно, какая именно из них бросила ошибку, и обработка становится мутной. В таких случаях полезнее дробить: несколько небольших do/catch вокруг самостоятельных шагов, либо вынос шагов в функции (но без фанатизма).

1
Задача
Swift SELF, 29 уровень, 2 лекция
Недоступна
Деление без нуля
Деление без нуля
1
Задача
Swift SELF, 29 уровень, 2 лекция
Недоступна
Год в анкете
Год в анкете
1
Задача
Swift SELF, 29 уровень, 2 лекция
Недоступна
Скидка по правилам
Скидка по правилам
1
Задача
Swift SELF, 29 уровень, 2 лекция
Недоступна
Список покупок
Список покупок
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ