web-dev-qa-db-fra.com

Pourquoi est-ce mauvais style de "sauver Exception => e" en Ruby?

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

Ne sauvez pas Exception. DÉJÀ. ou je vais te poignarder.

Pourquoi pas? Quelle est la bonne chose à faire?

864
John

TL; DR : utilisez plutôt StandardError pour la capture d’exception générale. Lorsque l'exception d'origine est à nouveau levée (par exemple, lors de la sauvegarde pour enregistrer uniquement l'exception), la récupération de Exception est probablement correcte.


Exception est la racine de hiérarchie des exceptions de Ruby , donc lorsque vous 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.

Sauver SignalException empêche le programme de répondre correctement aux signaux. Il sera impossible à tuer sauf par kill -9.

Sauver SyntaxError signifie que evals qui échouent le feront en silence.

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

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 la valeur par défaut. Faire

begin
  # iceberg!
rescue
  # lifeboats
end

ne sauve pas de Exception, il sauve de StandardError. Vous devez généralement spécifier quelque chose de plus spécifique que la valeur par défaut StandardError, mais le fait de sauver de Exception élargit la portée plutôt que de la réduire, et peut avoir des résultats catastrophiques et rendre la chasse aux bogues extrêmement difficile.


Si vous voulez sauver de StandardError et que vous avez besoin d'une variable exceptée, vous pouvez utiliser le formulaire suivant:

begin
  # iceberg!
rescue => e
  # lifeboats
end

qui équivaut à:

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

L’un des rares cas courants où il est sain de sauver de Exception est utilisé à des fins de journalisation/de génération de rapports, auquel cas vous devez immédiatement lever de nouveau l’exception:

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

La règle réelle est la suivante: ne jetez pas les exceptions. L’objectivité de l’auteur de votre citation est discutable, comme en témoigne le fait qu’elle se termine par

ou je vais te poignarder

Bien sûr, sachez que les signaux (par défaut) émettent des exceptions et que, normalement, les processus de longue durée sont terminés par un signal. Par conséquent, capturer Exception et non les exceptions de signal rendra votre programme très difficile à arrêter. Alors ne fais pas ça:

#! /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, supposons que vous ayez un serveur threadé et que vous voulez que toutes les exceptions ne le fassent pas:

  1. être ignoré (par défaut)
  2. arrêtez le serveur (ce qui arrive si vous dites thread.abort_on_exception = true).

Ceci 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 correspond à une variante du gestionnaire d'exceptions par défaut de Ruby, avec l'avantage qu'il ne tue pas également votre programme. Rails le fait dans son gestionnaire de requêtes.

Les exceptions de signal sont levées dans le thread principal. Les threads d'arrière-plan ne les auront pas, il est donc inutile d'essayer de les intercepter.

Ceci est particulièrement utile dans un environnement de production, où vous voulez pas que votre programme s’arrête tout simplement en cas de problème. Vous pouvez ensuite prendre les empilements de pile dans vos journaux et les ajouter à votre code pour traiter les exceptions spécifiques plus loin dans la chaîne d'appels et de manière plus harmonieuse.

Notez également qu’il existe un autre idiome Ruby qui a à peu près le même effet:

a = do_something rescue "something else"

Dans cette ligne, si do_something lève une exception, celle-ci est interceptée par Ruby, jetée et a est affectée à "something else".

En règle générale, ne faites pas cela, sauf dans des cas particuliers où vous savez vous n'avez pas besoin de vous inquiéter. Un exemple:

debugger rescue nil

La fonction debugger est un moyen plutôt agréable de définir un point d'arrêt dans votre code, mais si elle est exécutée en dehors d'un débogueur et de Rails, elle déclenche une exception. Maintenant théoriquement, vous ne devriez pas laisser le code de débogage traîner dans votre programme (pff! Personne ne le fait!) Mais vous voudrez peut-être le conserver pendant un certain temps pour une raison quelconque, mais ne pas exécuter continuellement votre débogueur.

Remarque:

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

    • sous Linux, dans un shell, tapez pgrep Ruby ou ps | grep Ruby, recherchez le PID de votre programme en cause, puis exécutez kill -9 <PID>.
    • sous Windows, utilisez le gestionnaire de tâches (CTRL-SHIFT-ESC), allez dans l'onglet "processus", trouvez votre processus, cliquez dessus avec le bouton droit de la souris et sélectionnez "Terminer le processus".
  2. Si vous travaillez avec le programme de quelqu'un d'autre qui, pour une raison quelconque, est parsemé de ces blocs ignore-exception, le placer en haut de la ligne principale est une exception possible:

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

    Cela force le programme à répondre aux signaux de terminaison normaux en terminant immédiatement, en contournant les gestionnaires d'exceptions, sans nettoyage. Donc, cela pourrait causer une perte de données ou similaire. Faites attention!

  3. Si vous avez besoin de 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.

79
Michael Slade

Disons que vous êtes dans une voiture (en cours d'exécution Ruby). Vous avez récemment installé un nouveau volant avec le système de mise à niveau par liaison radio (qui utilise eval), mais vous ne saviez pas que l'un des programmeurs s'était trompé de syntaxe.

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

def turn_left
  self.turn left:
end

oops! C'est probablement Pas bon ™, heureusement, Ruby lève un SyntaxError.

La voiture devrait s'arrêter immédiatement, n'est-ce pas?

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: exception capturée SyntaxError.

Info: Erreur enregistrée - Processus en cours.

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

bip bip

Avertissement: exception d'interruption capturée.

Info: Erreur enregistrée - Processus en cours.

Ouais - ça n'a pas beaucoup aidé. Vous êtes assez près du rail, vous devez donc garer la voiture (killing: SignalException).

bip bip

Avertissement: exception signalée exception capturée.

Info: Erreur enregistrée - Processus en cours.

À la dernière seconde, vous retirez les clés (kill -9), et la voiture s’arrête, vous vous enfoncez dans le volant (l’airbag ne peut pas se déployer car vous n’avez pas arrêté le programme gracieusement - vous l’avez interrompu. ), et l’ordinateur à l’arrière de votre voiture s’incline sur le siège qui se trouve devant. Une canette de Coca à moitié pleine se répand sur les papiers. Les courses à l'arrière sont broyées et la plupart sont recouvertes de jaune d'oeuf et de lait. La voiture doit être sérieusement réparée et nettoyée. (Perte de données)

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


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

Disons que vous êtes cette voiture et que vous voulez vous assurer que l'airbag se déploie si la voiture dépasse son moment d'arrêt de sécurité.

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

Voici l'exception à la règle: Vous pouvez intercepter Exceptionniquement si vous relancez l'exception. Donc, une meilleure règle consiste à ne jamais avaler Exception, et à toujours relancer l'erreur.

Toutefois, il est facile d'oublier l'ajout d'une opération de secours dans une langue telle que Ruby, et le fait de placer une déclaration d'assistance avant de soulever à nouveau une question donne l'impression que vous n'êtes pas assez sec. Et vous ne le faites pas voulez oublier la déclaration raise. Et si vous le faites, bonne chance pour essayer de trouver cette erreur.

Heureusement, Ruby est génial, vous pouvez simplement utiliser le mot clé ensure, ce qui garantit que le code est exécuté. Le mot-clé ensure exécutera le code, peu importe ce qui se passe - 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.exceeding_safe_stopping_momentum?
 end

Boom! Et ce code devrait fonctionner de toute façon. La seule raison pour laquelle vous devez utiliser rescue Exception => e est si vous avez besoin d'accéder à l'exception, ou si vous souhaitez que le code ne s'exécute que sur une exception. Et rappelez-vous de relancer l'erreur. À chaque fois.

Remarque: Comme @Niall l'a fait remarquer, assurez-vous que toujours s'exécute. C’est bien, car parfois votre programme peut vous mentir et ne pas lancer d’exceptions, même en cas de problème. Avec des tâches critiques, telles que le gonflage des airbags, vous devez vous assurer que cela se produit quoi qu'il arrive. Pour cette raison, il est judicieux de vérifier chaque fois que la voiture s’arrête, qu’une exception soit levée ou non. Même si gonfler des airbags est une tâche peu commune dans la plupart des contextes de programmation, cette tâche est plutôt courante dans la plupart des tâches de nettoyage.


TL; DR

Ne pas rescue Exception => e (et ne pas relancer l'exception) - ou vous pourriez quitter un pont.

62
Ben Aubin

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

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

Avaler les exceptions est mauvais, ne faites pas ceci.

45
Sergio Tulentsev

C'est un cas spécifique de la règle que vous ne devriez pas attraper aucun exception que vous ne savez pas comment gérer. Si vous ne savez pas comment vous en occuper, il est toujours préférable de laisser une autre partie du système s'en occuper.

9
Russell Borogove