web-dev-qa-db-fra.com

Déclenchez une exception personnalisée avec des arguments

Je définis une exception personnalisée sur un modèle dans Rails comme une sorte d'exception wrapper: (begin[code]rescue[raise custom exception]end)

Lorsque je lève l'exception, je voudrais lui transmettre quelques informations sur a) l'instance du modèle dont les fonctions internes déclenchent l'erreur, et b) l'erreur qui a été interceptée.

Il s'agit d'une méthode d'importation automatisée d'un modèle qui est renseigné par POST depuis une source de données étrangère.

tldr; Comment passer des arguments à une exception, étant donné que vous définissez vous-même l'exception? J'ai une méthode d'initialisation sur cette exception, mais la syntaxe raise semble accepter uniquement une classe et un message d'exception, aucun paramètre facultatif qui est transmis au processus d'instanciation.

44
Chris Keele

créez une instance de votre exception avec new:

class CustomException < StandardError
  def initialize(data)
    @data = data
  end
end
# => nil 
raise CustomException.new(bla: "blupp")
# CustomException: CustomException
65
phoet

Solution:

class FooError < StandardError
  attr_reader :foo

  def initialize(foo)
   super
   @foo = foo
  end
end

C'est le meilleur moyen si vous suivez le Rubocop Style Guide et passez toujours votre message comme deuxième argument à raise:

raise FooError.new('foo'), 'bar'

Vous pouvez obtenir foo comme ceci:

rescue FooError => error
  error.foo     # => 'foo'
  error.message # => 'bar'

Si vous souhaitez personnaliser le message d'erreur, écrivez:

class FooError < StandardError
  attr_reader :foo

  def initialize(foo)
   super
   @foo = foo
  end

  def message
    "The foo is: #{foo}"
  end
end

Cela fonctionne bien si foo est requis. Si vous voulez que foo soit un argument facultatif, continuez à lire.


Explication:

Passez votre message comme deuxième argument à raise

Comme le dit Rubocop Style Guide , le message et la classe d'exception doivent être fournis sous forme d'arguments distincts, car si vous écrivez:

raise FooError.new('bar')

Et si vous voulez passer une trace à raise, il n'y a aucun moyen de le faire sans passer deux fois le message:

raise FooError.new('bar'), 'bar', other_error.backtrace

Comme cette réponse le dit, vous devrez passer une trace arrière si vous souhaitez relancer une exception en tant que nouvelle instance avec la même trace arrière et un message ou des données différents.

Implémentation de FooError

Le nœud du problème est que si foo est un argument facultatif, il existe deux façons différentes de lever des exceptions:

raise FooError.new('foo'), 'bar', backtrace # case 1

et

raise FooError, 'bar', backtrace # case 2

et nous voulons que FooError fonctionne avec les deux.

Dans le cas 1, puisque vous avez fourni une instance d'erreur plutôt qu'une classe, raise définit 'bar' Comme message de l'instance d'erreur.

Dans le cas 2, raise instancie FooError pour vous et passe 'bar' Comme seul argument, mais il ne définit pas le message après l'initialisation comme dans le cas 1. Pour définir le message, vous devez appeler super dans FooError#initialize avec le message comme seul argument.

Ainsi, dans le cas 1, FooError#initialize Reçoit 'foo' Et dans le cas 2, il reçoit 'bar'. Il est surchargé et il n'y a en général aucun moyen de différencier ces cas. Il s'agit d'un défaut de conception dans Ruby. Donc, si foo est un argument facultatif, vous avez trois choix:

(a) acceptez que la valeur passée à FooError#initialize peut être foo ou un message.

(b) Utilisez uniquement le style de cas 1 ou de cas 2 avec raise mais pas les deux.

(c) Faites de foo un argument de mot-clé.

Si vous ne voulez pas que foo soit un argument de mot clé, je recommande (a) et mon implémentation de FooError ci-dessus est conçue pour fonctionner de cette façon.

Si vous raise a FooError en utilisant le style de casse 2, la valeur de foo est le message, qui est implicitement transmis à super. Vous aurez besoin d'une super(foo) explicite si vous ajoutez d'autres arguments à FooError#initialize.

Si vous utilisez un argument de mot clé (h/t réponse de Lemon Cat ) alors le code ressemble à:

class FooError < StandardError
  attr_reader :foo

  def initialize(message, foo: nil)
   super(message)
   @foo = foo
  end
end

Et l'augmentation ressemble à:

raise FooError, 'bar', backtrace
raise FooError(foo: 'foo'), 'bar', backtrace
14
Max Wallace

Voici un exemple de code ajoutant un code à une erreur:

class MyCustomError < StandardError
    attr_reader :code

    def initialize(code)
        @code = code
    end

    def to_s
        "[#{code}] #{super}"
    end
end

Et pour l'augmenter: raise MyCustomError.new(code), message

8
cyrilchampier

TL; DR 7 ans après cette question, je pense que la bonne réponse est:

class CustomException < StandardError
  attr_reader :extra
  def initialize(message=nil, extra: nil)
    super(message)
    @extra = extra
  end
end
# => nil 
raise CustomException.new('some message', extra: "blupp")

ATTENTION: vous obtiendrez des résultats identiques avec:

raise CustomException.new(extra: 'blupp'), 'some message'

mais c'est parce que Exception#exception(string) fait un #rb_obj_clone sur self, puis appelle exc_initialize (qui n'appelle PAS CustomException#initialize. De - error.c :

static VALUE
exc_exception(int argc, VALUE *argv, VALUE self)
{
    VALUE exc;

    if (argc == 0) return self;
    if (argc == 1 && self == argv[0]) return self;
    exc = rb_obj_clone(self);
    exc_initialize(argc, argv, exc);

    return exc;
}

Dans le dernier exemple de #raise Ci-dessus, un CustomException sera raise d avec message réglé sur "un message" et extra mis à "blupp" (car il s'agit d'un clone) mais [~ # ~] deux [~ # ~] CustomException sont effectivement créé: le premier par CustomException.new, et le second par #raise appelant #exception sur la première instance de CustomException, ce qui crée un second cloné CustomException.

Ma version de remix de danse étendue de pourquoi est à: https://stackoverflow.com/a/56371923/529948

1
Lemon Cat