Почему плохой стиль `спасать Exception => e` в Ruby?

895

Руби QuickRef Райана Дэвиса говорит (без объяснения причин):

Не спасайте Исключение. КОГДА-ЛИБО. или я тебя зарежу.

Почему бы нет? Что правильно сделать?

Джон
источник
35
Тогда вы, вероятно, могли бы написать свой собственный? :)
Серхио Туленцев
65
Мне очень неловко от призыва к насилию здесь. Это просто программирование.
Дарт Эгрегиус,
1
Взгляните на эту статью в Ruby Exception с хорошей иерархией исключений Ruby .
Атул Хандури
2
Потому что Райан Дэвис нанесет тебе удар. Так что дети. Никогда не спасайте исключения.
Mugen
7
@DarthEgregious Не могу сказать, шутишь ты или нет. Но я думаю, что это весело. (И это явно не серьезная угроза). Теперь каждый раз, когда я думаю о том, чтобы поймать «Исключение», я думаю, стоит ли его зарезать случайному парню в Интернете.
Стив Сетер

Ответы:

1375

TL; DR : использовать StandardErrorвместо этого для общего отслеживания исключений. Когда исходное исключение повторно вызывается (например, при спасении, чтобы регистрировать только исключение), спасение Exception, вероятно, в порядке.


Exceptionявляется корнем иерархии исключений в Ruby , поэтому , когда вы rescue Exceptionвы спасти от всего , в том числе подклассов , таких как SyntaxError, LoadErrorи Interrupt.

Спасение не Interruptпозволяет пользователю использовать CTRLCдля выхода из программы.

Спасение не SignalExceptionпозволяет программе правильно реагировать на сигналы. Это будет неубиваемо, кроме как kill -9.

Спасение SyntaxErrorозначает, evalчто неудачники будут делать это молча.

Все это можно показать, запустив эту программу, и пытается CTRLCили killему:

loop do
  begin
    sleep 1
    eval "djsakru3924r9eiuorwju3498 += 5u84fior8u8t4ruyf8ihiure"
  rescue Exception
    puts "I refuse to fail or be stopped!"
  end
end

Спасение от Exceptionдаже не по умолчанию. дела

begin
  # iceberg!
rescue
  # lifeboats
end

не спасает от Exception, спасает от StandardError. Как правило, вы должны указывать что-то более конкретное, чем значение по умолчанию StandardError, но спасение от Exception расширения, а не сужения области действия, может привести к катастрофическим результатам и сделать поиск ошибок чрезвычайно трудным.


Если у вас есть ситуация, когда вы хотите спастись, StandardErrorи вам нужна переменная с исключением, вы можете использовать эту форму:

begin
  # iceberg!
rescue => e
  # lifeboats
end

что эквивалентно:

begin
  # iceberg!
rescue StandardError => e
  # lifeboats
end

Один из немногих распространенных случаев Exception, из которых целесообразно спасать, - для целей регистрации / отчетности, и в этом случае вы должны немедленно повторно вызвать исключение:

begin
  # iceberg?
rescue Exception => e
  # do some logging
  raise # not enough lifeboats ;)
end
Эндрю Маршалл
источник
129
так что это все равно что ловить Throwableв java
фрик с трещоткой
53
Этот совет хорош для чистой среды Ruby. Но, к сожалению, ряд драгоценных камней создали исключения, которые непосредственно происходят от исключения. В нашей среде их 30: например, OpenID :: Server :: EncodingError, OAuth :: InvalidRequest, HTMLTokenizerSample. Это исключения, которые вы бы очень хотели поймать в стандартных блоках спасения. К сожалению, ничто в Ruby не препятствует и даже не препятствует тому, чтобы драгоценные камни наследовали непосредственно от Exception - даже наименование не интуитивно понятно.
Джонатан Шварц
20
@JonathanSwartz Тогда спасение от этих конкретных подклассов, а не исключение. Более конкретное почти всегда лучше и понятнее.
Эндрю Маршалл
22
@JonathanSwartz - я бы посоветовал создателям драгоценных камней изменить то, что наследует их исключение. Лично мне нравится, что в моих драгоценных камнях все исключения происходят от MyGemException, так что вы можете спасти это, если захотите.
Натан Лонг
12
Вы также можете ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]и тогдаrescue *ADAPTER_ERRORS => e
j_mcnally
83

Реальное правило: Не выбрасывать исключения. Объективность автора вашей цитаты сомнительна, о чем свидетельствует тот факт, что она заканчивается

или я тебя зарежу

Конечно, имейте в виду, что сигналы (по умолчанию) генерируют исключения, и обычно длительные процессы завершаются с помощью сигнала, поэтому перехват исключений и не прекращение исключений сигналов очень затруднит остановку вашей программы. Так что не делайте этого:

#! /usr/bin/ruby

while true do
  begin
    line = STDIN.gets
    # heavy processing
  rescue Exception => e
    puts "caught exception #{e}! ohnoes!"
  end
end

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

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

  1. игнорироваться (по умолчанию)
  2. остановите сервер (что произойдет, если вы скажете thread.abort_on_exception = true).

Тогда это вполне приемлемо в вашей ветке обработки соединений:

begin
  # do stuff
rescue Exception => e
  myLogger.error("uncaught #{e} exception while handling connection: #{e.message}")
    myLogger.error("Stack trace: #{backtrace.map {|l| "  #{l}\n"}.join}")
end

Вышесказанное работает с вариантом обработчика исключений по умолчанию в Ruby, с тем преимуществом, что он также не убивает вашу программу. Rails делает это в своем обработчике запросов.

Исключения сигналов возникают в основном потоке. Фоновые потоки не получат их, поэтому нет смысла пытаться их там поймать.

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

Также обратите внимание, что существует другая идиома Ruby, которая имеет почти такой же эффект:

a = do_something rescue "something else"

В этой строке, если do_somethingвозникает исключение, оно перехватывается Ruby, выбрасывается и aназначается "something else".

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

debugger rescue nil

debuggerФункция довольно хороший способ , чтобы установить точку останова в коде, но при работе вне отладчика, и Rails, он вызывает исключение. Теперь, теоретически, вы не должны оставлять отладочный код в своей программе (pff! Никто не делает этого!), Но вы можете захотеть оставить его там какое-то время, но не запускать отладчик постоянно.

Замечания:

  1. Если вы запустили чужую программу, которая ловит исключения сигналов и игнорирует их (скажем, код выше), то:

    • в Linux в оболочке введите pgrep rubyили ps | grep rubyнайдите PID вашей программы-нарушителя и запустите kill -9 <PID>.
    • в Windows используйте диспетчер задач ( CTRL- SHIFT- ESC), перейдите на вкладку «процессы», найдите свой процесс, щелкните его правой кнопкой мыши и выберите «Завершить процесс».
  2. Если вы работаете с чьей-либо программой, которая по какой-либо причине усеяна этими блоками игнорирования-исключения, то размещение этого в верхней части основной строки является одним из возможных отказов:

    %W/INT QUIT TERM/.each { |sig| trap sig,"SYSTEM_DEFAULT" }

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

  3. Если вам нужно сделать это:

    begin
      do_something
    rescue Exception => e
      critical_cleanup
      raise
    end

    вы можете сделать это:

    begin
      do_something
    ensure
      critical_cleanup
    end

    Во втором случае critical cleanupбудет вызываться каждый раз, независимо от того, выдано ли исключение.

Майкл Слэйд
источник
21
Извините, это неправильно. Сервер никогда не должен спасать Exception и ничего не делать, кроме как регистрировать его. Это сделает его неубиваемым, кроме как kill -9.
Джон
8
Ваши примеры в примечании 3 не являются равноправными: ensureзавещание будет выполняться независимо от того, было ли возбуждено исключение или нет, тогда как rescueзавещание будет выполняться только в том случае, если было возбуждено исключение.
Эндрю Маршалл
1
Они не / точно / эквивалентны, но я не могу понять, как кратко выразить эквивалентность таким образом, который не выглядит безобразно.
Майкл Слэйд
3
Просто добавьте еще один критический_колоночный вызов после блока begin / rescue в первом примере. Я согласен, что это не самый элегантный код, но, очевидно, второй пример - это элегантный способ сделать это, так что небольшая неэквивалентность является лишь частью примера.
gtd
3
«Даже не запускайте это, чтобы увидеть, работает ли оно». кажется плохим советом для кодирования ... Напротив, я бы посоветовал вам запустить его, увидеть, как он терпит неудачу, и самому понять, что если не получится, вместо того, чтобы слепо верить кому-то другому. Отличный ответ в любом случае :)
Huelbois
69

TL; DR

Не делайте rescue Exception => e(и не поднимайте заново исключение) - иначе вы можете сойти с моста.


Допустим, вы находитесь в машине (работает Руби). Вы недавно установили новое рулевое колесо с беспроводной системой обновления (которая использует eval), но вы не знали, что один из программистов испортил синтаксис.

Вы находитесь на мосту и понимаете, что идете немного к перилам, поэтому вы поворачиваете налево.

def turn_left
  self.turn left:
end

упс! Это, вероятно, не хорошо ™, к счастью, Руби поднимает SyntaxError.

Машина должна немедленно остановиться - верно?

Нет.

begin
  #...
  eval self.steering_wheel
  #...
rescue Exception => e
  self.beep
  self.log "Caught #{e}.", :warn
  self.log "Logged Error - Continuing Process.", :info
end

звуковой сигнал звуковой сигнал

Предупреждение: поймано исключение SyntaxError.

Информация: Зарегистрированная ошибка - продолжение процесса.

Вы заметили что - то не так, и вы хлопнуть на аварийных перерывов ( ^C: Interrupt)

звуковой сигнал звуковой сигнал

Предупреждение: исключение прерывания прерывания.

Информация: Зарегистрированная ошибка - продолжение процесса.

Да - это не очень помогло. Вы довольно близко к рельсам, поэтому вы ставите машину в парк ( killing:) SignalException.

звуковой сигнал звуковой сигнал

Предупреждение: поймано исключение SignalException.

Информация: Зарегистрированная ошибка - продолжение процесса.

В последнюю секунду вы вытаскиваете ключи ( kill -9), и машина останавливается, вы врезаетесь в руль (подушка безопасности не может надуваться, потому что вы не грациозно остановили программу - вы ее остановили), и компьютер в задней части вашего автомобиля врезается в сиденье перед ним. Наполовину полная банка кока-колы проливает на бумаги. Продукты в задней части измельчены, и большинство из них покрыты яичным желтком и молоком. Автомобиль нуждается в серьезном ремонте и чистке. (Потеря данных)

Надеюсь, у вас есть страховка (резервные копии). Ах да - потому что подушка безопасности не надулась, вы, вероятно, ранены (уволены и т. Д.).


Но ждать! Там вБольшепричины, почему вы можете захотеть использовать rescue Exception => e!

Допустим, вы тот автомобиль, и вы хотите убедиться, что подушка безопасности надувается, если автомобиль превышает безопасный момент остановки.

 begin 
    # do driving stuff
 rescue Exception => e
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
    raise
 end

Вот исключение из правила: Вы можете поймать его, Exception только если повторно вызовите исключение . Итак, лучшее правило - никогда не глотать Exceptionи всегда повторно выдавать ошибку.

Но добавление аварийного восстановления легко забыть на языке, подобном Ruby, и создание правильного выражения перед повторным поднятием проблемы выглядит немного неСУХОЙ. И вы не хотите забывать это raiseутверждение. И если вы это сделаете, удачи в попытке найти эту ошибку.

К счастью, Ruby великолепен, вы можете просто использовать ensureключевое слово, которое обеспечивает выполнение кода. ensureКлючевое слово будет работать код независимо от того , что - если исключение, если один не является, единственным исключением является , если мир заканчивается (или другие маловероятные события).

 begin 
    # do driving stuff
 ensure
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
 end

Boom! И этот код должен работать в любом случае. Единственная причина, по которой вы должны использовать rescue Exception => eэто то, что вам нужен доступ к исключению или если вы хотите, чтобы код выполнялся только для исключения. И не забудьте повторно поднять ошибку. Каждый раз.

Примечание: как указывал @Niall, всегда выполняйте . Это хорошо, потому что иногда ваша программа может лгать вам и не генерировать исключения, даже когда возникают проблемы. С критическими задачами, такими как надувные подушки безопасности, вы должны быть уверены, что это произойдет, несмотря ни на что. Из-за этого, проверяя каждый раз, когда автомобиль останавливается, выбрасывается ли исключение, хорошая идея. Несмотря на то, что накачивание подушек безопасности является довольно редкой задачей в большинстве контекстов программирования, на самом деле это довольно часто встречается в большинстве задач по очистке.

Бен Обин
источник
12
Хахаха! Это отличный ответ. Я в шоке, что никто не прокомментировал. Вы даете четкий сценарий, который делает все это действительно понятным. Ура! :-)
Джеймс Милани
@JamesMilani Спасибо!
Бен Обин
3
+ 💯 за этот ответ. Хотелось бы, чтобы я проголосовал не раз! Engineer
engineerDave
1
Понравился твой ответ!
Атул Вайбхав
3
Этот ответ пришел через 4 года после совершенно понятного и правильного принятого ответа, и вновь объяснил его абсурдным сценарием, который был скорее забавным, чем реалистичным. Извините, что я не в восторге, но это не реддит - для ответов важнее быть краткими и правильными, чем смешными. Кроме того, часть, ensureкасающаяся альтернативы, rescue Exceptionвводит в заблуждение - пример подразумевает, что они эквивалентны, но, как уже говорилось, ensureбудет происходить независимо от того, есть ли исключение или нет, поэтому теперь ваши подушки безопасности будут надуваться, потому что вы разогнались на 5 миль в час, даже если ничего не пошло не так.
Найл
47

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

Вы должны обрабатывать только исключения, которые вы знаете, как восстановить. Если вы не ожидаете какого-то исключения, не обрабатывайте его, громко вылетайте (записывайте подробности в журнал), затем диагностируйте журналы и исправляйте код.

Глотать исключения - это плохо, не делай этого.

Серхио Туленцев
источник
10

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

Рассел Борогове
источник
0

Я только что прочитал отличный пост в блоге об этом на honeybadger.io :

Исключение Руби против StandardError: Какая разница?

Почему вы не должны спасать исключение

Проблема со спасением Исключения состоит в том, что он фактически спасает каждое исключение, которое наследуется от Исключения. Который .... все они!

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

Вот несколько крупных:

  • SignalException :: Interrupt - если вы спасете это, вы не сможете выйти из приложения, нажав control-c.

  • ScriptError :: SyntaxError - Глотание синтаксических ошибок означает, что такие вещи, как put («Забыл что-то»), будут молча терпеть неудачу.

  • NoMemoryError - Хотите знать, что происходит, когда ваша программа продолжает работать после того, как она использует всю оперативную память? И я нет.

begin
  do_something()
rescue Exception => e
  # Don't do this. This will swallow every single exception. Nothing gets past it. 
end

Я предполагаю, что вы действительно не хотите проглотить ни одно из этих исключений системного уровня. Вы только хотите поймать все ваши ошибки уровня приложения. Исключения вызвали ВАШ код.

К счастью, есть простой способ сделать это.

calebkm
источник