Исключения и обработка ошибок

Программы ломаются: файл не найден, сеть недоступна, данные битые. Исключения — это управляемый способ реагировать на сбои, не превращая код в лабиринт проверок.
Суть: ошибки в Ruby — это объекты-исключения; их «бросают» через raise и «ловят» через begin/rescue; ensure выполняется всегда, а retry повторяет попытку.

Когда происходит ошибка, Ruby создаёт объект-исключение и начинает «всплывать» вверх по стеку вызовов, пока кто-нибудь его не поймает. Если не поймает никто — программа падает с трассировкой. Конструкция begin/rescue позволяет перехватить исключение и обработать его осмысленно.

begin
  result = 10 / 0
rescue ZeroDivisionError => e
  puts "Поймали ошибку: #{e.message}"
  result = 0
end
puts result   # => Поймали ошибку: divided by 0 ... 0

Разбор: ensure, raise и retry

ensure — блок, который выполнится в любом случае: была ошибка или нет. Это место для «уборки»: закрыть файл, освободить ресурс. raise бросает исключение вручную. А retry внутри rescue повторяет блок begin — полезно для сетевых запросов.

def fetch_data(attempts = 3)
  tries = 0
  begin
    tries += 1
    raise "сеть недоступна" if rand > 0.3   # имитация сбоя
    "данные получены"
  rescue => e
    retry if tries < attempts                # повторить
    "сдались после #{tries} попыток: #{e.message}"
  ensure
    puts "попытка ##{tries} завершена"        # выполнится всегда
  end
end
puts fetch_data

Как работает под капотом: иерархия исключений

Все исключения наследуются от класса Exception. Но ловить нужно не его, а StandardError и его потомков — это «обычные» ошибки программы. Голый rescue ловит именно StandardError, не трогая системные сигналы вроде прерывания по Ctrl+C. Свои ошибки создают наследованием от StandardError.

   Exception                       <- НЕ ловить целиком!
     |
     +-- StandardError             <- ловим это и потомков
     |     |
     |     +-- ZeroDivisionError
     |     +-- ArgumentError
     |     +-- TypeError
     |     +-- ВашаОшибка < StandardError
     |
     +-- SignalException (Ctrl+C)  <- системное, не ловим
     +-- NoMemoryError             <- системное

   raise бросает --> всплывает вверх --> первый rescue ловит

Та же идея try/except на Python:

# Та же логика на Python ▶
def fetch_data():
    try:
        return 10 / 0
    except ZeroDivisionError as e:
        print(f"Поймали ошибку: {e}")
        return 0
    finally:                       # аналог ensure
        print("блок завершён")

print(fetch_data())

Частые ошибки

  • rescue Exception. Перехват Exception ловит даже Ctrl+C и нехватку памяти — программу станет не остановить. Ловите StandardError (или просто rescue).
  • Глотать ошибки молча. Пустой rescue; end прячет проблему. Как минимум логируйте.
  • Исключения для обычного потока. Не используйте их вместо if: они дороги и сбивают читателя.

Best practices

  • Ловите конкретные классы ошибок, а не всё подряд — так вы обрабатываете именно то, что ожидали.
  • Создавайте собственные классы ошибок (class ValidationError < StandardError) для доменных сбоев — их удобно ловить точечно.
  • Освобождайте ресурсы в ensure, чтобы файлы и соединения закрывались даже при ошибке.

Глубже: исключения против возврата ошибок

Полезно понимать, что исключения — не единственный способ сообщить о проблеме, и опытные разработчики выбирают подход осознанно. Исключения хороши для действительно исключительных ситуаций: то, что нарушает нормальный ход программы и требует особой обработки на верхнем уровне. Но если «ошибка» — это ожидаемый и частый исход (пользователь ввёл неверный пароль, валидация формы не прошла), бросать исключение на каждый такой случай — дорого и зашумляет код. Здесь чище вернуть результат-значение: например, объект, у которого есть success? и errors, или просто nil при отсутствии. Это подталкивает вызывающий код явно проверять исход, а не полагаться на то, что «где-то наверху поймают». В экосистеме Ruby популярны паттерны вроде объектов-результатов (Result/Either), которые делают успех и провал равноправными ветвями. Правило-ориентир: исключения — для «что-то сломалось и это ненормально», возвращаемые значения — для «ожидаемо не получилось, и это часть бизнес-логики». Смешение этих подходов — частая причина либо лавины try/rescue, либо тихо проглоченных ошибок. Выбирайте инструмент под природу ситуации, а не по привычке.

Итог. Исключения — это объекты, которые всплывают по стеку. begin/rescue ловит их, ensure выполняется всегда, retry повторяет, а raise бросает. Ловите StandardError, а не Exception, и создавайте свои классы ошибок для домена.

Проверьте себя
1. Когда выполняется блок ensure?
AТолько если была ошибка
BТолько если ошибки не было
CВсегда — и при ошибке, и без неё
DНикогда
2. Почему не стоит писать rescue Exception?
AЭто синтаксическая ошибка
BТак перехватываются даже системные события вроде Ctrl+C и нехватки памяти — программу не остановить
CException нельзя поймать
DЭто медленно работает