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

895

Ruby QuickRef de Ryan Davis dit (sans explication):

Ne sauvez pas l'exception. DÉJÀ. ou je te poignarderai.

Pourquoi pas? Quelle est la bonne chose à faire?

John
la source
35
Alors vous pourriez probablement écrire le vôtre? :)
Sergio Tulentsev
65
Je suis très mal à l'aise avec l'appel à la violence ici. C'est juste de la programmation.
Dark Egregious
1
Jetez un oeil à cet article dans Ruby Exception avec une belle hiérarchie d'exception Ruby .
Atul Khanduri
2
Parce que Ryan Davis vous poignardera. Alors les enfants. Ne sauvez jamais d'exceptions.
Mugen
7
@DarthEgregious Je ne peux pas vraiment dire si vous plaisantez ou non. Mais je pense que c'est hilarant. (Et ce n'est évidemment pas une menace sérieuse). Maintenant, chaque fois que je pense à attraper Exception, je me demande s'il vaut la peine d'être poignardé par un gars au hasard sur Internet.
Steve Sether

Réponses:

1375

TL; DR : utilisez à la StandardErrorplace pour la capture d'exceptions générales. Lorsque l'exception d'origine est re-levée (par exemple lors d'un sauvetage pour enregistrer l'exception uniquement), le sauvetage Exceptionest probablement correct.


Exceptionest la racine de la hiérarchie des exceptions de Ruby , donc quand vous rescue Exceptionvous sauver de tout , y compris les sous - classes telles que SyntaxError, LoadErroret Interrupt.

Le sauvetage Interruptempêche l'utilisateur d'utiliser CTRLCpour quitter le programme.

Le sauvetage SignalExceptionempêche le programme de répondre correctement aux signaux. Ce sera impossible à tuer sauf par kill -9.

Le sauvetage SyntaxErrorsignifie que evalles échecs se feront en silence.

Tous ces éléments peuvent être affichés en exécutant ce programme, et en essayant de CTRLCou killil:

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

Le sauvetage Exceptionn'est même pas la valeur par défaut. Faire

begin
  # iceberg!
rescue
  # lifeboats
end

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


Si vous avez une situation dans laquelle vous souhaitez effectuer un sauvetage StandardErroret que 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

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

begin
  # iceberg?
rescue Exception => e
  # do some logging
  raise # not enough lifeboats ;)
end
Andrew Marshall
la source
129
c'est comme attraper Throwableen java
ratchet freak
53
Ce conseil est bon pour un environnement Ruby propre. Mais malheureusement, un certain nombre de gemmes ont créé des exceptions qui descendent directement d'Exception. Notre environnement en contient 30: par exemple OpenID :: Server :: EncodingError, OAuth :: InvalidRequest, HTMLTokenizerSample. Ce sont des exceptions que vous voudriez vraiment attraper dans les blocs de sauvetage standard. Malheureusement, rien dans Ruby n'empêche ou même décourage les gemmes d'hériter directement d'Exception - même la dénomination n'est pas intuitive.
Jonathan Swartz
20
@JonathanSwartz Puis sauvez de ces sous-classes spécifiques, pas Exception. Plus spécifique est presque toujours meilleur et plus clair.
Andrew Marshall
22
@JonathanSwartz - Je bogue les créateurs de gemmes pour changer l'héritage de leur exception. Personnellement, j'aime que mes gemmes aient toutes les exceptions dérivées de MyGemException, vous pouvez donc le sauver si vous le souhaitez.
Nathan Long
12
Vous pouvez aussi ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]et puisrescue *ADAPTER_ERRORS => e
j_mcnally
83

La vraie 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'elle se termine par

ou je te poignarderai

Bien sûr, sachez que les signaux (par défaut) lèvent des exceptions, et que les processus de longue durée se terminent par un signal, donc intercepter une exception et ne pas terminer sur des 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 lancez même pas cela pour voir si cela fonctionne.

Cependant, supposons que vous ayez un serveur threadé et que vous ne souhaitiez pas que toutes les exceptions:

  1. être ignoré (par défaut)
  2. arrêtez le serveur (ce qui arrive si vous dites 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 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 fils d'arrière-plan ne les obtiendront pas, il est donc inutile d'essayer de les attraper là-bas.

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

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_somethinglève une exception, il est attrapé par Ruby, jeté et aattribué "something else".

Généralement, ne faites pas cela, sauf dans des cas particuliers où vous savez que vous n'avez pas à vous inquiéter. Un exemple:

debugger rescue nil

le debugger fonction est un moyen plutôt agréable de définir un point d'arrêt dans votre code, mais si elle s'exécute en dehors d'un débogueur et de Rails, elle déclenche une exception. Maintenant, théoriquement, vous ne devriez pas laisser traîner du code de débogage dans votre programme (pff!

Remarque:

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

    • sous Linux, dans un shell, tapez pgrep rubyou ps | grep rubyrecherchez le PID de votre programme incriminé, puis exécutez kill -9 <PID>.
    • sous Windows, utilisez le Gestionnaire des tâches ( CTRL- SHIFT- 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, pour quelque raison que ce soit, est parsemé de ces blocs ignorer les exceptions, le mettre en haut de la ligne principale est une sortie possible:

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

    Cela oblige le programme à répondre aux signaux de terminaison normaux en se terminant immédiatement, en contournant les gestionnaires d'exceptions, sans nettoyage . Cela pourrait donc entraîner une perte de données ou similaire. Faites attention!

  3. Si vous devez le faire:

    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 cleanupsera appelé à chaque fois, qu'une exception soit levée ou non.

Michael Slade
la source
21
Désolé, c'est faux. Un serveur ne doit jamais sauver Exception et ne rien faire d'autre que de le journaliser. Cela le rendra impossible à tuer sauf par kill -9.
John
8
Vos exemples dans la note 3 ne sont pas équivalents, un test s'exécutera, ensurequ'il y ait une exception levée ou non, tandis que le test rescuene fonctionnera que si une exception a été déclenchée.
Andrew Marshall
1
Ils ne sont pas / exactement / équivalents mais je ne sais pas comment exprimer succinctement l'équivalence d'une manière qui n'est pas laide.
Michael Slade
3
Ajoutez simplement un autre appel critique_cleanup après le bloc begin / rescue dans le premier exemple. Je ne suis pas d'accord sur le code le plus élégant, mais évidemment le deuxième exemple est la manière élégante de le faire, donc un peu d'inélégance n'est qu'une partie de l'exemple.
gtd
3
"Ne lancez même pas ça pour voir si ça marche." semble un mauvais conseil pour le codage ... Au contraire, je vous conseille de l'exécuter, de le voir échouer et de comprendre par vous-même comment il échoue, au lieu de croire aveuglément quelqu'un d'autre.
Bonne
69

TL; DR

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


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

Vous êtes sur un pont et vous vous rendez compte que vous vous dirigez un peu vers la balustrade, alors tournez à gauche.

def turn_left
  self.turn left:
end

Oops! Ce n'est probablement pas bon ™, heureusement, Ruby soulève un 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: exception SyntaxError interceptée.

Info: Erreur enregistrée - Processus en cours.

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

bip Bip

Avertissement: exception d'interruption interceptée.

Info: Erreur enregistrée - Processus en cours.

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

bip Bip

Avertissement: exception SignalException intercepté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 claquez vers l'avant dans le volant (l'airbag ne peut pas se gonfler parce que vous n'avez pas gracieusement arrêté le programme - vous l'avez terminé), et l'ordinateur à l'arrière de votre voiture claque dans le siège en face d'elle. Une canette à moitié pleine de Coke déborde sur les papiers. L'épicerie à l'arrière est écrasée 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 ouais - parce que l'airbag ne s'est pas gonflé, vous êtes probablement blessé (être renvoyé, etc.).


Mais attendez! Il y aplusraisons pour lesquelles vous voudrez peut-être utiliser rescue Exception => e!

Disons que vous êtes cette voiture et que vous voulez vous assurer que l'airbag se gonfle si la voiture dépasse sa vitesse d'arrêt en toute 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 ne pouvez intercepter Exception que si vous relancez l'exception . Donc, une meilleure règle est de ne jamais avaler Exceptionet de toujours relancer l'erreur.

Mais l'ajout de sauvetage est à la fois facile à oublier dans un langage comme Ruby, et mettre une déclaration de sauvetage juste avant de soulever un problème semble un peu sec. Et vous ne voulez pas oublier la raisedéclaration. Et si vous le faites, bonne chance pour essayer de trouver cette erreur.

Heureusement, Ruby est génial, vous pouvez simplement utiliser le ensuremot - clé, qui s'assure que le code s'exécute. Le ensuremot-clé exécutera le code quoi qu'il arrive - si une exception est levée, si ce n'est pas le cas, la seule exception étant la fin du monde (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 que vous devez utiliser rescue Exception => eest si vous avez besoin d'accéder à l'exception ou si vous souhaitez uniquement que le code s'exécute sur une exception. Et n'oubliez pas de relancer l'erreur. À chaque fois.

Remarque: Comme l'a souligné @Niall, assurez-vous qu'il fonctionne toujours . C'est bien parce que parfois votre programme peut vous mentir et ne pas lever d'exceptions, même lorsque des problèmes surviennent. Avec les tâches critiques, comme le gonflage des airbags, vous devez vous assurer que cela se produit quoi qu'il arrive. Pour cette raison, vérifier chaque fois que la voiture s'arrête, si une exception est levée ou non, est une bonne idée. Même si le gonflage des airbags est une tâche peu courante dans la plupart des contextes de programmation, cela est en fait assez courant avec la plupart des tâches de nettoyage.

Ben Aubin
la source
12
Hahahaha! C'est une excellente réponse. Je suis choqué que personne n'ait commenté. Vous donnez un scénario clair qui rend le tout vraiment compréhensible. À votre santé! :-)
James Milani
@JamesMilani Merci!
Ben Aubin
3
+ 💯 pour cette réponse. J'aimerais pouvoir voter plus d'une fois! 😂
engineerDave
1
Apprécié votre réponse!
Atul Vaibhav
3
Cette réponse est venue 4 ans après la réponse acceptée parfaitement compréhensible et correcte, et l'a ré-expliquée avec un scénario absurde conçu plus pour être amusant que réaliste. Désolé d'être un buzzkill, mais ce n'est pas reddit - il est plus important que les réponses soient succinctes et correctes que drôles. De plus, la partie sur ensurecomme alternative à rescue Exceptionest trompeuse - l'exemple implique qu'ils sont équivalents, mais comme indiqué ensurese produira qu'il y ait une exception ou non, alors maintenant vos airbags se déploieront parce que vous avez dépassé 5 mph, même si rien ne s'est mal passé.
Niall
47

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

Vous ne devez gérer que les exceptions dont 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 des exceptions est mauvais, ne faites pas ça.

Sergio Tulentsev
la source
10

C'est un cas spécifique de la règle que vous ne devez pas détecter d' exception que 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 l'attraper et le gérer.

Russell Borogove
la source
0

Je viens de lire un excellent article de blog à ce sujet sur honeybadger.io :

Ruby Exception vs StandardError: Quelle est la différence?

Pourquoi vous ne devriez pas sauver l'exception

Le problème du sauvetage d'Exception est qu'il sauve en fait chaque exception qui hérite d'Exception. Ce qui est ... tous!

C'est un problème car il existe des exceptions qui sont utilisées en interne par Ruby. Ils n'ont rien à voir avec votre application, et les avaler entraînera de mauvaises choses.

En voici quelques-uns:

  • SignalException :: Interrupt - Si vous sauvez ceci, vous ne pouvez pas quitter votre application en appuyant sur control-c.

  • ScriptError :: SyntaxError - Avaler des erreurs de syntaxe signifie que des choses comme put ("J'ai oublié quelque chose) échouent silencieusement.

  • NoMemoryError - Vous voulez savoir ce qui se passe lorsque votre programme continue de fonctionner après avoir utilisé toute la RAM? Moi non plus.

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

Je suppose que vous ne voulez pas vraiment avaler ces exceptions au niveau du système. Vous souhaitez uniquement détecter toutes vos erreurs au niveau de l'application. Les exceptions ont provoqué VOTRE code.

Heureusement, il existe un moyen simple d'y parvenir.

calebkm
la source