Question Pourquoi est-ce mauvais style de `sauver Exception => e` dans Ruby?


Ryan Davis's Ruby QuickRef dit (sans explication):

Ne pas sauver l'exception. DÉJÀ. ou je vais te poignarder.

Pourquoi pas? Quelle est la bonne chose à faire?


800
2018-04-06 19:17


origine


Réponses:


TL; DR: Utilisation StandardError à la place, pour l'exception générale d'attraper. Lorsque l'exception d'origine est relancée (par exemple, lors de la sauvegarde pour enregistrer l'exception uniquement), le sauvetage Exception est probablement correct.


Exception est la racine de La hiérarchie des exceptions de Ruby, alors quand tu rescue Exception vous sauvez de tout, y compris les sous-classes telles que SyntaxError, LoadError, et Interrupt.

Sauvetage Interrupt empêche l'utilisateur d'utiliser CTRLC pour quitter le programme.

Sauvetage SignalException empêche le programme de répondre correctement aux signaux. Ce sera inutilisable sauf par kill -9.

Sauvetage SyntaxError signifie que evals qui échouera le fera en silence.

Tout cela peut être montré en exécutant ce programme, et en essayant de CTRLC ou kill il:

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

Sauver de Exception n'est même pas le défaut. Faire

begin
  # iceberg!
rescue
  # lifeboats
end

ne sauve pas de Exception, il sauve de StandardError. Vous devriez généralement spécifier quelque chose de plus spécifique que la valeur par défaut StandardErrormais en sauvant de Exception  élargit la portée plutôt que de la rétrécir, et peut avoir des résultats catastrophiques et rendre la chasse aux insectes extrêmement difficile.


Si vous avez une situation où vous voulez sauver de StandardError et vous avez besoin d'une variable à l'exception, vous pouvez utiliser ce formulaire:

begin
  # iceberg!
rescue => e
  # lifeboats
end

ce qui équivaut à:

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

Un des rares cas courants où il est sain de sauver Exception est à des fins de journalisation / reporting, auquel cas vous devriez immédiatement sur-relancer l'exception:

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

1240
2018-04-06 19:38



le réal La règle est: Ne jetez pas les exceptions. L'objectivité de l'auteur de votre citation est discutable, comme en témoigne le fait qu'il se termine par

ou je vais te poignarder

Bien sûr, sachez que les signaux (par défaut) émettent des exceptions, et que les processus normalement longs sont terminés par un signal, donc attraper Exception et ne pas terminer sur les exceptions de signal rendra votre programme très difficile à arrêter. Donc, ne faites pas ceci:

#! /usr/bin/ruby

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

Non, vraiment, ne le fais pas. Ne lance même pas ça pour voir si ça marche.

Cependant, disons que vous avez un serveur threadé et que vous voulez que toutes les exceptions ne le soient pas:

  1. être ignoré (par défaut)
  2. arrête le serveur (ce qui arrive si tu dis thread.abort_on_exception = true).

Ensuite, cela est parfaitement acceptable dans votre thread de gestion de connexion:

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

Ce qui précède est une variante du gestionnaire d'exception par défaut de Ruby, avec l'avantage de ne pas tuer votre programme. Rails fait cela dans son gestionnaire de requêtes.

Les exceptions de signal sont déclenchées dans le thread principal. Les threads d'arrière-plan ne les obtiendront pas, donc il ne sert à rien d'essayer de les attraper là.

Ceci est particulièrement utile dans un environnement de production, où vous faites ne pas voulez que votre programme s'arrête simplement quand quelque chose ne va pas. Ensuite, vous pouvez prendre les décharges de la pile dans vos journaux et les ajouter à votre code pour traiter des exceptions spécifiques plus loin dans la chaîne d'appel et de manière plus gracieuse.

Notez aussi qu'il existe un autre idiome de Ruby qui a à peu près le même effet:

a = do_something rescue "something else"

Dans cette ligne, si do_something soulève une exception, il est attrapé par Ruby, jeté, et a est assigné "something else".

Généralement, ne faites pas cela, sauf dans des cas spéciaux où vous connaître tu n'as pas besoin de t'inquiéter. Un exemple:

debugger rescue nil

le debugger La fonction est un moyen plutôt agréable de définir un point d'arrêt dans votre code, mais si vous exécutez en dehors d'un débogueur, et Rails, il déclenche une exception. Maintenant, en théorie, vous ne devriez pas laisser de code de débogage dans votre programme (pff! Nobody ne le fait pas!) Mais vous pourriez vouloir le garder pendant un certain temps pour une raison quelconque, mais pas continuellement votre débogueur.

Remarque:

  1. Si vous avez exécuté le programme de quelqu'un d'autre qui capture les exceptions de signal et les ignore, (disons le code ci-dessus) alors:

    • sous Linux, dans un shell, tapez pgrep ruby, ou ps | grep ruby, recherchez le PID de votre programme incriminé, puis exécutez kill -9 <PID>.
    • dans Windows, utilisez le Gestionnaire des tâches (CTRL-DÉCALAGE-ESC), allez dans l'onglet "processus", trouvez votre processus, faites un clic droit dessus et sélectionnez "Terminer le processus".
  2. Si vous travaillez avec le programme de quelqu'un d'autre qui est, pour une raison quelconque, parsemé de ces blocs d'exception-exception, alors mettre ceci en haut de la ligne principale est une possibilité de dérobade:

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

    Cela provoque le programme à répondre aux signaux de terminaison normaux en terminant immédiatement, en contournant les gestionnaires d'exception, sans nettoyage. Cela peut donc provoquer une perte de données ou similaire. Faites attention!

  3. Si vous devez faire ceci:

    begin
      do_something
    rescue Exception => e
      critical_cleanup
      raise
    end
    

    vous pouvez réellement faire ceci:

    begin
      do_something
    ensure
      critical_cleanup
    end
    

    Dans le second cas, critical cleanup sera appelé à chaque fois, qu'une exception soit levée ou non.


73
2018-04-07 05:30



Disons que vous êtes dans une voiture (Ruby en cours d'exécution). Vous avez récemment installé un nouveau volant avec le système de mise à niveau over-the-air (qui utilise eval), mais vous ne connaissiez pas l'un des programmeurs foiré sur la syntaxe.

Vous êtes sur un pont, et vous réalisez que vous allez un peu vers la balustrade, alors vous tournez à gauche.

def turn_left
  self.turn left:
end

Oops! C'est probablement Pas bon, heureusement, Ruby soulève une SyntaxError.

La voiture devrait s'arrêter immédiatement - non?

Nan.

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

Avertissement: Caught SyntaxError Exception.

Info: Erreur enregistrée - Processus continu.

Vous remarquez que quelque chose ne va pas, et vous claquez les pauses d'urgence (^C: Interrupt)

bip Bip

Avertissement: Exception d'interruption interceptée.

Info: Erreur enregistrée - Processus continu.

Ouais - ça n'a pas beaucoup aidé. Vous êtes assez près du rail, alors vous mettez la voiture dans le parc (killing: SignalException).

bip Bip

Avertissement: Exception SignalException interceptée.

Info: Erreur enregistrée - Processus continu.

À la dernière seconde, vous retirez les clés (kill -9), et la voiture s'arrête, vous claquez dans le volant (l'airbag ne peut pas se gonfler parce que vous n'avez pas arrêté le programme gracieusement - vous l'avez terminé), et l'ordinateur à l'arrière de votre voiture claque dans le siège devant lui. Une canette de Coca à moitié pleine se répand sur les papiers. Les produits d'épicerie dans le dos sont écrasés et la plupart sont couverts de jaune d'œuf et de lait. La voiture a besoin de réparations et de nettoyages sérieux. (Perte de données)

J'espère que vous avez une assurance (sauvegardes). Ah oui - parce que l'airbag ne s'est pas gonflé, vous êtes probablement blessé (se faire virer, etc).


Mais attendez! Il y a plus raisons pour lesquelles vous pourriez vouloir utiliser rescue Exception => e!

Disons que vous êtes cette voiture, et vous voulez vous assurer que l'airbag se gonfle si la voiture roule à plus de 5mph avant de s'arrêter.

 begin 
    # do driving stuff
 rescue Exception => e
    self.airbags.inflate if self.speed >= 5.mph 
    raise
 end

Voici l'exception à la règle: Vous pouvez attraper Exception  seulement si vous relancez l'exception. Donc, une meilleure règle est de ne jamais avaler Exception, et toujours re-augmenter l'erreur.

Mais l'ajout de secours est facile à oublier dans une langue comme Ruby, et mettre une déclaration de sauvetage juste avant de re-soulever un problème se sent un peu non-DRY. Et toi ne pas vouloir oublier le raise déclaration. Et si vous le faites, bonne chance en essayant de trouver cette erreur.

Heureusement, Ruby est génial, vous pouvez simplement utiliser le ensure mot-clé, qui s'assure que le code s'exécute. le ensure mot-clé exécutera le code n'importe quoi - si une exception est levée, si ce n'est pas le cas, la seule exception est si le monde se termine (ou d'autres événements improbables).

 begin 
    # do driving stuff
 ensure
    self.airbags.inflate if self.speed >= 5.mph 
 end

Boom! Et ce code devrait fonctionner de toute façon. La seule raison pour laquelle vous devriez utiliser rescue Exception => e est si vous avez besoin d'accéder à l'exception, ou si vous voulez que le code s'exécute sur une exception. Et n'oubliez pas de re-augmenter l'erreur. À chaque fois. Ou vous aurez 3 personnes vous poignardant (y compris votre patron).


TL; DR

Ne pas rescue Exception => e (et ne pas re-augmenter l'exception) - ou vous pourrait chasser un pont.


46
2018-01-31 23:55



Parce que cela capture toutes les exceptions. Il est peu probable que votre programme puisse récupérer tout d'eux.

Vous ne devez gérer que les exceptions dont vous savez comment récupérer. Si vous n'anticipez pas un certain type d'exception, ne le gérez pas, plantez fort (notez les détails dans le journal), puis diagnostiquez les journaux et corrigez le code.

Avaler des exceptions est mauvais, ne le faites pas.


43
2018-04-06 19:21



C'est un cas particulier de la règle que vous ne devriez pas prendre tout exception, vous ne savez pas comment gérer. Si vous ne savez pas comment le gérer, il est toujours préférable de laisser une autre partie du système attraper et gérer.


8
2018-04-06 23:54



Cela vous cachera également des bogues, par exemple si vous avez mal tapé un nom de méthode:

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

1
2018-03-25 09:11