Por que é um estilo ruim `resgatar Exception => e` em Ruby?

Rubis QuickRef, de Ryan Davis, diz (sem explicação):

Não resgate Exceção. SEMPRE. ou eu vou te esfaquear.

Por que não? Qual é a coisa certa a fazer?

TL; DR : Use StandardError vez de captura geral de exceção. Quando a exceção original é re-aumentada (por exemplo, ao resgatar para registrar apenas a exceção), o resgate da Exception provavelmente está correto.


Exception é a raiz da hierarquia de exceções do Ruby , portanto, quando você rescue Exception você salva de tudo , incluindo subclasss como SyntaxError , LoadError e Interrupt .

Interrupt impede que o usuário use CTRL C para sair do programa.

Resgatar SignalException impede que o programa responda corretamente aos sinais. Será impossível kill -9 não ser por kill -9 .

Rescuing SyntaxError significa que eval s que falham farão isso silenciosamente.

Tudo isso pode ser mostrado executando este programa e tentando CTRL C ou kill lo:

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

Resgatar da Exception não é mesmo o padrão. Fazendo

 begin # iceberg! rescue # lifeboats end 

não resgata do Exception , ele resgata do StandardError . Geralmente, você deve especificar algo mais específico do que o padrão StandardError , mas o resgate de Exception amplia o escopo em vez de restringi-lo, e pode ter resultados catastróficos e tornar a busca de bugs extremamente difícil.


Se você tem uma situação onde você quer resgatar do StandardError e você precisa de uma variável com a exceção, você pode usar este formulário:

 begin # iceberg! rescue => e # lifeboats end 

o que equivale a:

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

Um dos poucos casos comuns em que é sensato resgatar da Exception é para fins de registro / relatório, e nesse caso você deve reaumentar a exceção imediatamente:

 begin # iceberg? rescue Exception => e # do some logging raise e # not enough lifeboats ;) end 

A regra real é: não jogue exceções. A objetividade do autor da sua citação é questionável, como evidenciado pelo fato de que ela termina com

ou eu vou te esfaquear

É claro, esteja ciente de que os sinais (por padrão) lançam exceções, e normalmente os processos de longa duração são finalizados através de um sinal, então pegar Exceção e não terminar em exceções de sinal fará com que seu programa seja muito difícil de parar. Então não faça isso:

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

Não, realmente, não faça isso. Nem execute isso para ver se funciona.

No entanto, digamos que você tenha um servidor encadeado e deseja que todas as exceções não:

  1. ser ignorado (o padrão)
  2. parar o servidor (o que acontece se você disser thread.abort_on_exception = true ).

Então isso é perfeitamente aceitável no seu segmento de manipulação de conexão:

 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 

O acima funciona para uma variação do manipulador de exceção padrão do Ruby, com a vantagem de que ele também não mata o seu programa. O Rails faz isso em seu manipulador de solicitações.

Exceções de sinal são levantadas no thread principal. Os tópicos de segundo plano não os conseguirão, então não há sentido em tentar pegá-los lá.

Isso é particularmente útil em um ambiente de produção, em que você não deseja que seu programa simplesmente pare sempre que algo der errado. Então você pode pegar os dumps de pilha em seus logs e adicionar ao seu código para lidar com uma exceção específica mais abaixo na cadeia de chamadas e de uma maneira mais elegante.

Note também que há outro idioma Ruby que tem o mesmo efeito:

 a = do_something rescue "something else" 

Nesta linha, se do_something uma exceção, ela será capturada por Ruby, descartada e a designada "something else" .

Geralmente, não faça isso, exceto em casos especiais em que você sabe que não precisa se preocupar. Um exemplo:

 debugger rescue nil 

A function debugger é uma maneira bastante interessante de definir um ponto de interrupção em seu código, mas se estiver executando fora de um depurador e Rails, isso gerará uma exceção. Agora, teoricamente, você não deveria deixar código de debugging em seu programa (pff !, ninguém faz isso!), Mas você pode querer mantê-lo lá por algum tempo, por algum motivo, mas não executar continuamente o seu depurador.

Nota:

  1. Se você executou o programa de outra pessoa que detecta exceções de sinais e os ignora (diga o código acima), então:

    • no Linux, em um shell, digite pgrep ruby ou ps | grep ruby ps | grep ruby , procure pelo PID do seu programa ofensor e execute kill -9 .
    • no Windows, use o Gerenciador de Tarefas ( CTRLSHIFTESC ), vá para a aba “processos”, encontre o seu processo, clique com o botão direito e selecione “Finalizar processo”.
  2. Se você estiver trabalhando com o programa de outra pessoa, que é, por qualquer motivo, salpicado com esses blocos de exceção de ignorar, então colocar isso no topo da linha principal é uma saída possível:

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

    Isso faz com que o programa responda aos sinais normais de terminação ao finalizar imediatamente, ignorando os manipuladores de exceção, sem limpeza . Por isso, pode causar perda de dados ou similar. Seja cuidadoso!

  3. Se você precisa fazer isso:

     begin do_something rescue Exception => e critical_cleanup raise end 

    você pode realmente fazer isso:

     begin do_something ensure critical_cleanup end 

    No segundo caso, critical cleanup será chamada toda vez, seja ou não lançada uma exceção.

Digamos que você esteja em um carro (rodando Ruby). Você instalou recentemente um novo volante com o sistema de atualização over-the-air (que usa o eval ), mas você não sabia que um dos programadores estava confuso na syntax.

Você está em uma ponte, e percebe que está indo um pouco para o parapeito, então vire à esquerda.

 def turn_left self.turn left: end 

oops! Isso é provavelmente Not Good ™, por sorte, Ruby gera um SyntaxError .

O carro deve parar imediatamente – certo?

Não.

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

bip Bip

Aviso: exceção SyntaxError capturada.

Info: Logged Error – Continuing Process.

Você percebe que algo está errado e bate nas interrupções de emergência ( ^C : Interrupt )

bip Bip

Aviso: exceção de interrupção capturada.

Info: Logged Error – Continuing Process.

Sim, isso não ajudou muito. Você está bem perto do trilho, então você coloca o carro no estacionamento ( kill : SignalException ).

bip Bip

Aviso: captura de exceção SignalException.

Info: Logged Error – Continuing Process.

No último segundo, você tira as chaves ( kill -9 ), e o carro para, você bate no volante (o airbag não pode inflar porque você não parou o programa graciosamente – você o terminou), e o computador na parte de trás do seu carro bate no assento em frente a ele. Uma lata meio cheia de Coca-Cola transborda os papéis. Os mantimentos nas costas são esmagados e a maioria está coberta de gema de ovo e leite. O carro precisa de reparos e limpeza sérios. (Perda de dados)

Espero que você tenha seguro (backups). Ah sim – porque o airbag não inchou, você provavelmente está machucado (sendo demitido, etc).


Mas espere! Há Mais razões pelas quais você pode querer usar rescue Exception => e !

Vamos dizer que você é esse carro, e você quer ter certeza de que o airbag infla se o carro estiver excedendo seu momento de parada seguro.

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

Aqui está a exceção à regra: Você só pode capturar Exception se você aumentar novamente a exceção . Portanto, uma regra melhor é nunca engolir Exception e sempre reaumentar o erro.

Mas adicionar o resgate é fácil de esquecer em uma linguagem como o Ruby, e colocar uma declaração de resgate antes de re-levantar um problema parece um pouco não-seco. E você não quer esquecer a declaração de raise . E se você fizer isso, boa sorte tentando encontrar esse erro.

Felizmente, Ruby é incrível, você pode simplesmente usar a palavra-chave ensure , que garante que o código seja executado. A palavra-chave ensure executará o código não importando o que – se uma exceção for lançada, se uma não for, a única exceção será se o mundo terminar (ou outros events improváveis).

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

Estrondo! E esse código deve ser executado de qualquer maneira. O único motivo pelo qual você deve usar rescue Exception => e é se você precisar de access à exceção ou se quiser que o código seja executado em uma exceção. E lembre-se de reaumentar o erro. Toda vez.

Nota: Como apontado @Niall, assegure-se de que sempre seja executado. Isso é bom porque às vezes seu programa pode mentir para você e não lançar exceções, mesmo quando ocorrem problemas. Com tarefas críticas, como inflar airbags, você precisa ter certeza de que isso acontece, não importa o quê. Por causa disso, verificar sempre que o carro pára, se uma exceção é lançada ou não, é uma boa ideia. Mesmo que inflar airbags seja uma tarefa incomum na maioria dos contextos de programação, isso é bastante comum na maioria das tarefas de limpeza.


TL; DR

Não rescue Exception => e (e não re-aumente a exceção) – ou você pode sair de uma ponte.

Porque isso captura todas as exceções. É improvável que seu programa possa se recuperar de qualquer um deles.

Você deve lidar apenas com exceções que você sabe como se recuperar. Se você não antecipar um certo tipo de exceção, não a manipule, falhe alto (escreva detalhes no log), então diagnostique os logs e corrija o código.

Exceder exceções é ruim, não faça isso.

Esse é um caso específico da regra de que você não deve pegar nenhuma exceção que você não saiba como manipular. Se você não sabe como lidar com isso, é sempre melhor deixar alguma outra parte do sistema capturar e lidar com isso.

Isso também esconderá erros de você, por exemplo, se você digitou errado o nome de um método:

 def my_fun "my_fun" end begin # you mistypped my_fun to my_func my_func # my_func() rescue Exception # rescued NameError (or NoMethodError if you called method with parenthesis) end