Comment ajouter des informations à un message d'exception sans changer sa classe en ruby?
L'approche que j'utilise actuellement est
strings.each_with_index do |string, i|
begin
do_risky_operation(string)
rescue
raise $!.class, "Problem with string number #{i}: #{$!}"
end
end
Idéalement, j'aimerais également conserver la trace.
Y a-t-il un meilleur moyen?
Pour relancer l'exception et modifier le message, tout en préservant la classe d'exception et sa trace, procédez comme suit:
strings.each_with_index do |string, i|
begin
do_risky_operation(string)
rescue Exception => e
raise $!, "Problem with string number #{i}: #{$!}", $!.backtrace
end
end
Ce qui donnera:
# RuntimeError: Problem with string number 0: Original error message here
# backtrace...
Ce n'est pas beaucoup mieux, mais vous pouvez simplement relancer l'exception avec un nouveau message:
raise $!, "Problem with string number #{i}: #{$!}"
Vous pouvez également obtenir vous-même un objet exception modifié avec la méthode exception
:
new_exception = $!.exception "Problem with string number #{i}: #{$!}"
raise new_exception
Voici un autre moyen:
class Exception
def with_extra_message extra
exception "#{message} - #{extra}"
end
end
begin
1/0
rescue => e
raise e.with_extra_message "you fool"
end
# raises an exception "ZeroDivisionError: divided by 0 - you fool" with original backtrace
(révisé pour utiliser la méthode exception
en interne, merci @Chuck)
Mon approche serait de extend
l'erreur rescue
d avec un module anonyme qui étend la méthode message
de l'erreur:
def make_extended_message(msg)
Module.new do
@@msg = msg
def message
super + @@msg
end
end
end
begin
begin
raise "this is a test"
rescue
raise($!.extend(make_extended_message(" that has been extended")))
end
rescue
puts $! # just says "this is a test"
puts $!.message # says extended message
end
De cette façon, vous ne saisissez aucune autre information dans l’exception (c’est-à-dire sa backtrace
).
Je mets mon vote que Ryan Heneise réponse devrait être la acceptée.
Il s'agit d'un problème courant dans les applications complexes et la préservation de la trace d'origine est souvent si critique que nous avons pour cela une méthode d'utilitaire dans notre module d'assistance ErrorHandling
.
L'un des problèmes que nous avons découverts est qu'essayer de générer des messages plus significatifs lorsqu'un système est dans un état de désordre entrainerait la génération d'exceptions à l'intérieur du gestionnaire d'exceptions lui-même, ce qui nous a conduit à renforcer notre fonction utilitaire comme suit:
def raise_with_new_message(*args)
ex = args.first.kind_of?(Exception) ? args.shift : $!
msg = begin
sprintf args.shift, *args
rescue Exception => e
"internal error modifying exception message for #{ex}: #{e}"
end
raise ex, msg, ex.backtrace
end
Quand ça va bien
begin
1/0
rescue => e
raise_with_new_message "error dividing %d by %d: %s", 1, 0, e
end
vous recevez un message bien modifié
ZeroDivisionError: error dividing 1 by 0: divided by 0
from (irb):19:in `/'
from (irb):19
from /Users/sim/.rvm/rubies/Ruby-2.0.0-p247/bin/irb:16:in `<main>'
Quand ça va mal
begin
1/0
rescue => e
# Oops, not passing enough arguments here...
raise_with_new_message "error dividing %d by %d: %s", e
end
vous ne perdez toujours pas la trace de la grande image
ZeroDivisionError: internal error modifying exception message for divided by 0: can't convert ZeroDivisionError into Integer
from (irb):25:in `/'
from (irb):25
from /Users/sim/.rvm/rubies/Ruby-2.0.0-p247/bin/irb:16:in `<main>'
Je me rends compte que je suis avec 6 ans de retard à cette fête, mais ... je pensais avoir compris Ruby le traitement des erreurs jusqu'à cette semaine et ai rencontré cette question. Bien que les réponses soient utiles, il existe un comportement non évident (et non documenté) qui peut être utile aux futurs lecteurs de ce fil. Tout le code a été exécuté sous Ruby v2.3.1.
@Andrew Grimm demande
Comment ajouter des informations à un message d'exception sans changer sa classe en ruby?
et fournit ensuite un exemple de code:
raise $!.class, "Problem with string number #{i}: #{$!}"
Je pense que c'est critical pour souligner que cela n'ajoute PAS d'informations à l'objet d'instance d'erreur d'origine , mais crée à la place un objet NEW avec la même classe.
@BoosterStage dit
Pour relancer l'exception et modifier le message ...
mais encore une fois, le code fourni
raise $!, "Problem with string number #{i}: #{$!}", $!.backtrace
lèvera une nouvelle instance de la classe d'erreur quelle que soit la classe référencée par $ !, mais elle sera not la même instance que $ !.
La différence entre le code de @Andrew Grimm et l'exemple de @ BoosterStage réside dans le fait que le premier argument de #raise
dans le premier cas est un Class
name__, alors que dans le deuxième cas, il s'agit d'une instance de (probablement) StandardError
name__. La différence est importante car la documentation de Kernel # raise indique:
Avec un seul argument String, déclenche une RuntimeError avec la chaîne en tant que message. Sinon, le premier paramètre devrait être le nom d'une classe d'exception (ou un objet qui renvoie un objet d'exception lors de l'envoi d'un message d'exception).
Si un seul argument est donné et qu'il s'agit d'une instance d'objet d'erreur, cet objet sera raise
name__d IF La méthode #exception
de cet objet hérite ou implémente le comportement par défaut défini dans Exception # exception (string) :
Sans argument, ou si l'argument est identique à celui du destinataire, retournez le destinataire. Sinon, créez un nouvel objet exception de la même classe que le destinataire, mais avec un message égal à string.to_str.
Comme beaucoup devineraient:
...
catch StandardError => e
raise $!
...
soulève la même erreur référencée par $ !, la même chose que simplement appeler:
...
catch StandardError => e
raise
...
mais probablement pas pour les raisons que l'on pourrait penser. Dans ce cas, l'appel à raise
est NOT soulève simplement l'objet dans $!
... il soulève le résultat de $!.exception(nil)
, qui dans ce cas se produit être $!
.
Pour clarifier ce comportement, considérons ce code jouet:
class TestError < StandardError
def initialize(message=nil)
puts 'initialize'
super
end
def exception(message=nil)
puts 'exception'
return self if message.nil? || message == self
super
end
end
L'exécuter (c'est la même chose que l'échantillon de @Andrew Grimm que j'ai cité ci-dessus):
2.3.1 :071 > begin ; raise TestError, 'message' ; rescue => e ; puts e ; end
résulte en:
initialize
message
Donc, une erreur TestError était initialize
name__d, rescue
name__d et son message a été imprimé. Jusqu'ici tout va bien. Un deuxième test (analogue à l'exemple de @ BoosterStage cité ci-dessus):
2.3.1 :073 > begin ; raise TestError.new('foo'), 'bar' ; rescue => e ; puts e ; end
Les résultats quelque peu surprenants:
initialize
exception
bar
Ainsi, TestError
était initialize
name__d avec 'foo', mais alors #raise
a appelé #exception
sur le premier argument (une instance de TestError
name__) et a transmis le message de 'bar' créez une seconde instance de TestError
name__, qui est finalement levée .
TIL.
De plus, comme @Sim, je suis very soucieux de préserver tout contexte de trace arrière, mais au lieu d'implémenter un gestionnaire d'erreurs personnalisé comme son raise_with_new_message
, Ruby Exception#cause
a mon dos: chaque fois que je veux intercepter une erreur, l'enroulez dans une erreur spécifique au domaine et ensuite relance that erreur, j'ai toujours la trace d'origine disponible via #cause
sur l’erreur générée spécifique au domaine.
Le but de tout cela est que - comme @Andrew Grimm - je veux soulever des erreurs avec plus de contexte; Plus précisément, je souhaite uniquement générer des erreurs spécifiques à un domaine à partir de certains points de mon application pouvant comporter de nombreux modes de défaillance liés au réseau. Ensuite, mes erreurs peuvent être signalées pour traiter les erreurs de domaine au plus haut niveau de mon application et j'ai tout le contexte dont j'ai besoin pour la journalisation/la création de rapports en appelant #cause
de manière récursive jusqu'à la "cause première".
J'utilise quelque chose comme ça:
class BaseDomainError < StandardError
attr_reader :extra
def initialize(message = nil, extra = nil)
super(message)
@extra = extra
end
end
class ServerDomainError < BaseDomainError; end
Ensuite, si j'utilise quelque chose comme Faraday pour passer des appels vers un service distant REST, je peux envelopper toutes les erreurs possibles dans une erreur propre à un domaine et transmettre des informations supplémentaires (ce qui, selon moi, est la question initiale de ce service). fil):
class ServiceX
def initialize(foo)
@foo = foo
end
def get_data(args)
begin
# This method is not defined and calling it will raise an error
make_network_call_to_service_x(args)
rescue StandardError => e
raise ServerDomainError.new('error calling service x', binding)
end
end
end
Oui, c'est vrai: je viens littéralement de réaliser que je peux définir les informations extra
sur le binding
pour saisir tous les vars locaux définis au moment où ServerDomainError
est instancié/levé. Ce code de test:
begin
ServiceX.new(:bar).get_data(a: 1, b: 2)
rescue
puts $!.extra.receiver
puts $!.extra.local_variables.join(', ')
puts $!.extra.local_variable_get(:args)
puts $!.extra.local_variable_get(:e)
puts eval('self.instance_variables', $!.extra)
puts eval('self.instance_variable_get(:@foo)', $!.extra)
end
affichera:
exception
#<ServiceX:0x00007f9b10c9ef48>
args, e
{:a=>1, :b=>2}
undefined method `make_network_call_to_service_x' for #<ServiceX:0x00007f9b10c9ef48 @foo=:bar>
@foo
bar
Désormais, un contrôleur Rails appelant ServiceX n'a pas particulièrement besoin de savoir que ServiceX utilise Faraday (ou gRPC, ou autre), il passe simplement l'appel et gère BaseDomainError
name__. Encore une fois: à des fins de journalisation, un seul gestionnaire au niveau supérieur peut consigner de manière récursive tous les #cause
s de toutes les erreurs interceptées, et pour toute instance de BaseDomainError
dans la chaîne d'erreurs, il peut également enregistrer les valeurs de extra
name__, y compris les variables locales. extraites de la binding
name __ (s) encapsulée.
J'espère que cette tournée a été aussi utile pour les autres que pour moi. J'ai beaucoup appris.
UPDATE: Skiptrace semble ajouter les liaisons aux erreurs Ruby.
Voir aussi cet autre article pour obtenir des informations sur la manière dont la mise en œuvre de Exception#exception
va cloner l'objet (instance de copie variables).
Voici ce que j'ai fini par faire:
Exception.class_eval do
def prepend_message(message)
mod = Module.new do
define_method :to_s do
message + super()
end
end
self.extend mod
end
def append_message(message)
mod = Module.new do
define_method :to_s do
super() + message
end
end
self.extend mod
end
end
Exemples:
strings = %w[a b c]
strings.each_with_index do |string, i|
begin
do_risky_operation(string)
rescue
raise $!.prepend_message "Problem with string number #{i}:"
end
end
=> NoMethodError: Problem with string number 0:undefined method `do_risky_operation' for main:Object
et:
pry(main)> exception = 0/0 rescue $!
=> #<ZeroDivisionError: divided by 0>
pry(main)> exception = exception.append_message('. With additional info!')
=> #<ZeroDivisionError: divided by 0. With additional info!>
pry(main)> exception.message
=> "divided by 0. With additional info!"
pry(main)> exception.to_s
=> "divided by 0. With additional info!"
pry(main)> exception.inspect
=> "#<ZeroDivisionError: divided by 0. With additional info!>"
Cela ressemble à la réponse de Mark Rushakoff mais:
to_s
au lieu de message
car par défaut message
est défini simplement comme to_s
(au moins en Ruby 2.0 et 2.2 où je l’ai testé)extend
pour vous au lieu de demander à l'appelant d'effectuer cette étape supplémentaire.define_method
et une fermeture pour que la variable locale message
puisse être référencée. Lorsque j’ai essayé d’utiliser une classe variable @@message
, elle a averti: "attention: accès aux variables de classe depuis le niveau le plus élevé" (Voir cette question : "Puisque vous ne créez pas de classe avec le mot clé class, votre variable de classe est définie sur Object
, pas [votre module anonyme] ")Caractéristiques:
to_s
, message
et inspect
répondent tous correctementraise $!, …, $!.backtrace
). Cela était important pour moi car l'exception était transmise à ma méthode de journalisation, ce n'était pas quelque chose que j'avais sauvé moi-même.