Иерархия ошибок в Ruby, а также какие исключения нужно бросать, а какие - ловить

26 августа 2011, обновлена 30 августа 2011

Никогда так не делай

Исключения — единственный механизм обработки ошибок в Ruby. Это одновременно и очень мощный, и опасный инструмент.

Поэтому важно правильно пользоваться исключениями, и в первую очередь — понимать, какие классы исключений нужно бросать, и какие — ловить.

Почему ловить Exception плохо?

Exception — базовый класс всей классовой иерархии исключений. (Чтобы ее посмотреть, можно выполнить скрипт, расположенный в статье Ruby’s Exception hierarchy). Это значит, что вообще все ошибки в Ruby наследуют класс Exception.

Например, при синтаксической ошибке бросается SyntaxError. Наследник Exception. А при выходе из программы – SystemExit (очень изящный подход, по-моему):

begin
  exit
rescue Exception
  puts $!.inspect # #<SystemExit: exit>
end
puts 'And it was the Nth time I died' # все еще работает!

Таким образом, перехват Exception грозит неожиданными сбоями в работе скрипта.

Поэтому rescue без параметра перехватывает не Exception, а StandardError, пропуская критические ошибки.

Почему бросать Exception плохо?

Место пользовательским исключениям в Ruby – класс RuntimeError. Если бросать ошибки класса Exception, то вышестоящие обработчики могут их не понять, и пропустить дальше.

Плохой тон.

Хорошо: ловить только ожидаемые исключения

Перехватывай только те классы исключений, которые ты можешь обработать, либо те, которые можешь с уверенностью проигнорировать.

Всегда лучше пропустить лишнее исключение, чем не заметить неправильно перехваченную ошибку.

Хорошо: использовать короткую строковую форму raise

Лаконичный код — правильный код: raise "message" равносильно raise RuntimeError.new("message"). Именно такую запись лучше использовать на скорую руку. Напомню, что RuntimeError — наиболее подходящее место для пользовательских ошибок.

begin
  raise "I'm Batman!"
rescue
  puts $!.inspect # #<RuntimeError: I'm Batman!>
end

Хорошо: создавать свои классы исключений

С помощью своих классов исключений ты даешь возможность скрипту-потребителю ловить только ожидаемые исключения.

Не вижу противопоказаний для того, чтоб создавать новый класс на каждую отдельную ошибку, если, конечно, не лень.

Лучше всего создавать свою под-иерархию классов:

RuntimeError
  FooLibrary::Error
    FooLibrary::BarError
    FooLibrary::BazError

Исключения из исключений?

Hoptoad Airbrake ловит все исключения, чтобы сообщать даже о синтаксических ошибках.

На случай апокалипсиса можно бросить и class EndOfWorldError < Exception.

Из всех правил есть исключения, и из правил про исключения — тоже. Главное не противоречить логике и не забывать их документировать .



Пять комментариев. Напиши еще один
  1. 8a2ece950414f3cce736d7d08183cbec # 30 августа 2011 Дмитрий Нестерюк написал:

    «Поэтому raise без параметра перехватывает» – Может имелось введу rescue? Иначе я не понимаю что raise может перехватывать, оно ведь бросает ошибку, а не перехватывает.

    1. 777894ea5153122bfa6b83f5bbf23622 # 30 августа 2011 Леонид Шевцов (автор) написал:

      Да, конечно. Спасибо.

  2. 84037edfd250c344b1db997b9eaf96cb # 30 августа 2011 rezwyi написал:

    Леонид, спасибо. Простые истины :)

  3. 2737edf5bdb56d9a5a0ea7f480306693 # 27 марта 2012 Сергей написал:

    Спасибо за статью!
    Прошу объяснить:

    Место пользовательским исключениям в Ruby – класс RuntimeError. Если бросать ошибки класса Exception, то вышестоящие обработчики могут их не понять, и пропустить дальше.
    Почему «могут их не понять»?

    1. 777894ea5153122bfa6b83f5bbf23622 # 29 марта 2012 Леонид Шевцов (автор) написал:

      Ну например:

      > def a; raise RuntimeError.new('foo'); end
      > def b; raise Exception.new('bar'); end
      > c = a rescue 'default'
      => 'default'
      > c = b rescue 'default'
      Exception: bar
      

      Наследовать Exception вместо RuntimeError — все равно, что вызывать пожарников, даже если всего-навсего задымилась урна.

(нужна разметка?)

  • **жирный**
  • > цитата

отменить