Por que é péssimo `resgatar Exception => e` em Ruby?

895

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

Não salve Exceção. SEMPRE. ou eu te apunhalarei.

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

John
fonte
35
Então você provavelmente poderia escrever o seu próprio? :)
Sergio Tulentsev 6/04/12
65
Estou muito desconfortável com a chamada à violência aqui. É apenas programação.
Darth Egregious
1
Dê uma olhada neste artigo no Ruby Exception com uma bela hierarquia de exceções do Ruby .
Atul Khanduri 23/11
2
Porque Ryan Davis vai apunhalá-lo. Então crianças. Nunca salve exceções.
Mugen 31/05
7
@DarthEgregious Eu realmente não sei dizer se você está brincando ou não. Mas acho hilário. (E obviamente não é uma ameaça séria). Agora, toda vez que penso em capturar Exception, considero se vale a pena ser esfaqueado por um cara aleatório na Internet.
Steve Sether 8/06

Respostas:

1375

TL; DR : use StandardErrorpara captura de exceção geral. Quando a exceção original é gerada novamente (por exemplo, ao resgatar para registrar apenas a exceção), o resgateException provavelmente está correto.


Exceptioné a raiz da hierarquia exceção de Ruby , então quando você rescue Exceptionvocê salva de tudo , incluindo subclasses tais como SyntaxError, LoadError, e Interrupt.

O resgate Interruptevita que o usuário use CTRLCpara sair do programa.

O resgate SignalExceptionimpede que o programa responda corretamente aos sinais. Será inviável, exceto por kill -9.

Resgatando SyntaxError significa que os evalque falharem o farão silenciosamente.

Tudo isso pode ser mostrado executando este programa e tentando CTRLCou kill:

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

O resgate de Exceptionnem é o padrão. Fazendo

begin
  # iceberg!
rescue
  # lifeboats
end

não resgata Exception, resgata de StandardError. Você geralmente deve especificar algo mais específico do que o padrão StandardError, mas resgatando a partir Exception amplia escopo vez de estreitá-lo, e pode ter resultados catastróficos e tornar extremamente difícil a busca de bugs.


Se você tem uma situação em que deseja resgatar StandardErrore precisa de uma variável com a exceção, pode usar este formulário:

begin
  # iceberg!
rescue => e
  # lifeboats
end

que é equivalente a:

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

Um dos poucos casos comuns em que é prudente resgatar Exceptioné para fins de registro / relatório; nesse caso, você deve imediatamente levantar novamente a exceção:

begin
  # iceberg?
rescue Exception => e
  # do some logging
  raise # not enough lifeboats ;)
end
Andrew Marshall
fonte
129
por isso é como pegar Throwableem java
aberração catraca
53
Este conselho é bom para um ambiente Ruby limpo. Infelizmente, porém, várias gemas criaram exceções que descendem diretamente da exceção. Nosso ambiente possui 30 deles: por exemplo, OpenID :: Server :: EncodingError, OAuth :: InvalidRequest, HTMLTokenizerSample. Essas são exceções que você gostaria de capturar em blocos de resgate padrão. Infelizmente, nada no Ruby impede ou mesmo desencoraja a gema de herdar diretamente do Exception - mesmo o nome não é intuitivo.
Jonathan Swartz
20
@ JonathanSwartz Em seguida, salve essas subclasses específicas, não Exception. Mais específico é quase sempre melhor e mais claro.
Andrew Marshall
22
@ JonathanSwartz - Gostaria de incomodar os criadores de gemas para alterar o que sua exceção herda. Pessoalmente, gosto que minhas jóias tenham todas as exceções descendentes de MyGemException, para que você possa resgatá-las, se quiser.
Nathan Long
12
Você também pode ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]e depoisrescue *ADAPTER_ERRORS => e
j_mcnally
83

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

ou eu vou apunhalá-lo

Obviamente, esteja ciente de que os sinais (por padrão) geram exceções e, normalmente, os processos de execução longa são encerrados por meio de um sinal; portanto, capturar Exceção e não encerrar em exceções de sinal dificultará a interrupção do programa. 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, sério, não faça isso. Nem execute isso para ver se funciona.

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

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

Isso é perfeitamente aceitável no segmento de manipulação de conexões:

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 exemplo acima funciona com 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 geradas no encadeamento principal. Os threads de plano de fundo não os receberão, portanto, não faz sentido tentar pegá-los lá.

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

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

a = do_something rescue "something else"

Nesta linha, se houver do_somethinguma exceção, ela é capturada por Ruby, jogada fora e aé atribuída "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

o debugger função é uma maneira bastante agradável de definir um ponto de interrupção no seu código, mas se estiver executando fora de um depurador e do Rails, isso gera uma exceção. Agora, teoricamente, você não deve deixar o código de depuração no seu programa (pff! Ninguém faz isso!), Mas você pode mantê-lo por um tempo por algum motivo, mas não executar continuamente o seu depurador.

Nota:

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

    • no Linux, em um shell, digite pgrep rubyou ps | grep rubyprocure o PID do seu programa incorreto e execute kill -9 <PID>.
    • no Windows, use o Gerenciador de tarefas ( CTRL- SHIFT- ESC), vá para a guia "processos", localize seu processo, clique com o botão direito do mouse e selecione "Finalizar processo".
  2. Se você estiver trabalhando com o programa de outra pessoa que, por qualquer motivo, estiver repleto desses blocos de exceção de ignorar, colocar isso no topo da linha principal é uma possível cópia:

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

    Isso faz com que o programa responda aos sinais normais de finalização, finalizando imediatamente, ignorando os manipuladores de exceção, sem limpeza . Portanto, pode causar perda de dados ou similar. Seja cuidadoso!

  3. Se você precisar 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 cleanupserá chamado sempre, independentemente de uma exceção ser lançada ou não.

Michael Slade
fonte
21
Desculpe, isso está errado. Um servidor nunca deve resgatar a exceção e não fazer nada além de registrá-la. Isso o tornará inviável, exceto por kill -9.
John John
8
Seus exemplos na nota 3 não são equivalentes; um ensureserá executado independentemente de haver uma exceção gerada ou não, enquanto o rescueserá executado apenas se uma exceção for gerada.
Andrew Marshall
1
Eles não são / exatamente / equivalentes, mas não consigo descobrir como expressar sucintamente a equivalência de uma maneira que não é feia.
Michael Slade
3
Basta adicionar outra chamada critical_cleanup após o bloco de início / resgate no primeiro exemplo. Não concordo com o código mais elegante, mas obviamente o segundo exemplo é a maneira elegante de fazê-lo, portanto, um pouco de deselegância é apenas parte do exemplo.
Gtd #
3
"Nem execute isso para ver se funciona." parece um péssimo conselho para a codificação ... Pelo contrário, aconselho você a executá-lo, vê-lo falhar e entender por si mesmo como se falha, em vez de acreditar cegamente em outra pessoa. Grande resposta de qualquer maneira :)
huelbois
69

TL; DR

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


Digamos que você esteja em um carro (executando Ruby). Recentemente, você instalou um novo volante com o sistema de atualização sem fio (que usa eval), mas não sabia que um dos programadores estragou a sintaxe.

Você está em uma ponte e percebe que está indo um pouco em direção ao corrimão, então vire à esquerda.

def turn_left
  self.turn left:
end

oops! Provavelmente isso não é bom ™, felizmente, Ruby levanta a 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 detectada.

Info: Erro registrado - Processo contínuo.

Você percebe que algo está errado, e você pisar os freios de emergência ( ^C: Interrupt)

bip Bip

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

Info: Erro registrado - Processo contínuo.

Sim - isso não ajudou muito. Você está bem perto do trilho, então você estaciona o carro ( killing:) SignalException.

bip Bip

Aviso: Exceção detectada de SignalException.

Info: Erro registrado - Processo contínuo.

No último segundo, você puxa as teclas ( kill -9) e o carro para, bate no volante (o airbag não pode ser inflado porque você não interrompeu graciosamente o programa - você o encerrou) e o computador na parte de trás do seu carro bate no banco em frente a ele. Uma lata pela metade de Coca-Cola derrama sobre os papéis. Os mantimentos na parte de trás são esmagados, e a maioria é coberta com 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 inflou, você provavelmente está ferido (ser demitido, etc.).


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

Digamos que você é o carro e deseja garantir que o airbag se esgote se o carro estiver excedendo seu momento de parada segura.

 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ê pode capturar Exception apenas se você aumentar novamente a exceção . Portanto, uma regra melhor é nunca engolir Exceptione sempre aumentar novamente o erro.

Mas é fácil esquecer o acréscimo de resgate em um idioma como Ruby, e colocar uma declaração de resgate logo antes de voltar a levantar um problema parece um pouco não-SECO. E você não quer esquecer a raiseafirmação. E se o fizer, boa sorte tentando encontrar esse erro.

Felizmente, Ruby é incrível, você pode simplesmente usar a ensurepalavra - chave, o que garante que o código seja executado. A ensurepalavra-chave executará o código, não importa o quê - se uma exceção for lançada, se não houver, a única exceção será se o mundo terminar (ou outros eventos 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 que você deve usar rescue Exception => eé se você precisa acessar a exceção ou se deseja que o código seja executado apenas em uma exceção. E lembre-se de aumentar novamente o erro. Toda vez.

Nota: Como o @Niall apontou, verifique sempre a execução. Isso é bom porque, às vezes, seu programa pode mentir para você e não gerar exceções, mesmo quando ocorrem problemas. Em tarefas críticas, como inflar airbags, você precisa garantir que isso aconteça, não importa o quê. Por isso, verificar sempre que o carro parar, se uma exceção é lançada ou não, é uma boa idéia. Embora inflar airbags seja uma tarefa pouco comum na maioria dos contextos de programação, isso é bastante comum na maioria das tarefas de limpeza.

Ben Aubin
fonte
12
Hahahaha! Esta é uma ótima resposta. Estou chocado que ninguém tenha comentado. Você fornece um cenário claro que torna tudo realmente compreensível. Felicidades! :-)
James Milani
@JamesMilani Thank you!
Ben Aubin
3
+ 💯 para esta resposta. Gostaria de poder votar mais de uma vez! 😂
engineerDave
1
Gostei da sua resposta!
Atul Vaibhav
3
Essa resposta veio quatro anos após a resposta aceita perfeitamente compreensível e correta, e a explicou novamente com um cenário absurdo projetado mais para ser divertido do que realista. Desculpe por ser um buzzkill, mas isso não é reddit - é mais importante que as respostas sejam sucintas e corretas do que engraçadas. Além disso, a parte ensurealternativa rescue Exceptioné enganosa - o exemplo implica que são equivalentes, mas, como afirmado ensure, acontecerá se há uma exceção ou não, então agora seus airbags inflarão porque você ultrapassou os 5 km / h, mesmo que nada tenha dado errado.
Niall
47

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 das quais sabe se recuperar. Se você não antecipar um certo tipo de exceção, não lide com isso, bata alto (grave detalhes no log), faça o diagnóstico dos logs e corrija o código.

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

Sergio Tulentsev
fonte
10

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

Russell Borogove
fonte
0

Acabei de ler um ótimo post sobre isso no honeybadger.io :

Exceção de Ruby vs StandardError: Qual é a diferença?

Por que você não deve resgatar a exceção

O problema com o resgate da exceção é que ele realmente resgata todas as exceções herdadas da exceção. Qual é .... todos eles!

Isso é um problema, porque existem algumas exceções usadas internamente pelo Ruby. Eles não têm nada a ver com seu aplicativo, e engoli-los fará com que coisas ruins aconteçam.

Aqui estão alguns dos grandes:

  • SignalException :: Interrupt - Se você resgatar isso, não poderá sair do aplicativo pressionando control-c.

  • ScriptError :: SyntaxError - Engolir erros de sintaxe significa que coisas como puts ("Esqueci alguma coisa) falharão silenciosamente.

  • NoMemoryError - Quer saber o que acontece quando o seu programa continua sendo executado depois que ele usa toda a RAM? Nem eu.

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

Suponho que você realmente não queira engolir nenhuma dessas exceções no nível do sistema. Você deseja capturar apenas todos os erros no nível do aplicativo. As exceções causaram SEU código.

Felizmente, há uma maneira fácil de fazer isso.

calebkm
fonte