Raise custom Exception with arguments

前端 未结 6 1807
礼貌的吻别
礼貌的吻别 2020-12-23 13:36

I\'m defining a custom Exception on a model in rails as kind of a wrapper Exception: (begin[code]rescue[raise custom exception]end)

When I raise the Exc

6条回答
  •  粉色の甜心
    2020-12-23 14:24

    Solution:

    class FooError < StandardError
      attr_reader :foo
    
      def initialize(foo)
       super
       @foo = foo
      end
    end
    

    This is the best way if you follow the Rubocop Style Guide and always pass your message as the second argument to raise:

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

    You can get foo like this:

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

    If you want to customize the error message then write:

    class FooError < StandardError
      attr_reader :foo
    
      def initialize(foo)
       super
       @foo = foo
      end
    
      def message
        "The foo is: #{foo}"
      end
    end
    

    This works well if foo is required. If you want foo to be an optional argument, then keep reading.


    Explanation:

    Pass your message as the second argument to raise

    As the Rubocop Style Guide says, the message and the exception class should be provided as separate arguments because if you write:

    raise FooError.new('bar')
    

    And want to pass a backtrace to raise, there is no way to do it without passing the message twice:

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

    As this answer says, you will need to pass a backtrace if you want to re-raise an exception as a new instance with the same backtrace and a different message or data.

    Implementing FooError

    The crux of the problem is that if foo is an optional argument, there are two different ways of raising exceptions:

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

    and

    raise FooError, 'bar', backtrace # case 2
    

    and we want FooError to work with both.

    In case 1, since you've provided an error instance rather than a class, raise sets 'bar' as the message of the error instance.

    In case 2, raise instantiates FooError for you and passes 'bar' as the only argument, but it does not set the message after initialization like in case 1. To set the message, you have to call super in FooError#initialize with the message as the only argument.

    So in case 1, FooError#initialize receives 'foo', and in case 2, it receives 'bar'. It's overloaded and there is no way in general to differentiate between these cases. This is a design flaw in Ruby. So if foo is an optional argument, you have three choices:

    (a) accept that the value passed to FooError#initialize may be either foo or a message.

    (b) Use only case 1 or case 2 style with raise but not both.

    (c) Make foo a keyword argument.

    If you don't want foo to be a keyword argument, I recommend (a) and my implementation of FooError above is designed to work that way.

    If you raise a FooError using case 2 style, the value of foo is the message, which gets implicitly passed to super. You will need an explicit super(foo) if you add more arguments to FooError#initialize.

    If you use a keyword argument (h/t Lemon Cat's answer) then the code looks like:

    class FooError < StandardError
      attr_reader :foo
    
      def initialize(message, foo: nil)
       super(message)
       @foo = foo
      end
    end
    

    And raising looks like:

    raise FooError, 'bar', backtrace
    raise FooError(foo: 'foo'), 'bar', backtrace
    

提交回复
热议问题