Исключения и обработка ошибок
Программы ломаются: файл не найден, сеть недоступна, данные битые. Исключения — это управляемый способ реагировать на сбои, не превращая код в лабиринт проверок.
Суть: ошибки в 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, и создавайте свои классы ошибок для домена.