Raise custom Exception with arguments

67,001

Solution 1

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

Solution 2

create an instance of your exception with new:

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

Solution 3

Here is a sample code adding a code to an error:

class MyCustomError < StandardError
    attr_reader :code

    def initialize(code)
        @code = code
    end

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

And to raise it: raise MyCustomError.new(code), message

Solution 4

TL;DR 7 years after this question, I believe the correct answer is:

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")

WARNING: you will get identical results with:

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

but that is because Exception#exception(string) does a #rb_obj_clone on self, and then calls exc_initialize (which does NOT call CustomException#initialize. From 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;
}

In the latter example of #raise up above, a CustomException will be raised with message set to "a message" and extra set to "blupp" (because it is a clone) but TWO CustomException objects are actually created: the first by CustomException.new, and the second by #raise calling #exception on the first instance of CustomException which creates a second cloned CustomException.

My extended dance remix version of why is at: https://stackoverflow.com/a/56371923/5299483

Share:
67,001
Chris Keele
Author by

Chris Keele

Tech tinkerer, web enveloper, professional digresser.

Updated on July 09, 2022

Comments

  • Chris Keele
    Chris Keele almost 2 years

    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 Exception, I'd like to pass it some info about a) the instance of the model whose internal functions raise the error, and b) the error that was caught.

    This is going on an automated import method of a model that gets populated by POST request to from foreign datasource.

    tldr; How can one pass arguments to an Exception, given that you define the Exception yourself? I have an initialize method on that Exception but the raise syntax seems to only accept an Exception class and message, no optional parameters that get passed into the instantiation process.

  • Chris Keele
    Chris Keele about 11 years
    I've been using this for a year now, and thought I'd add: now every time I want to do this and forget how, I take a peek at cancan's exceptions to remind myself. The last error follows very good form for more complicated exceptions.
  • Martin Konecny
    Martin Konecny almost 11 years
    You should be extending StandardError class - not Exception class. See here: skorks.com/2009/09/ruby-exceptions-and-exception-handling
  • phoet
    phoet over 9 years
    @vladCovaliov why would it fail? message is just empty
  • vladCovaliov
    vladCovaliov over 9 years
    You should always add message = nil as your first arguments and call super(message) otherwise something like raise CustomError, :some_message will not set the message correctly.
  • plombix
    plombix almost 8 years
    i' m on the same kind of problem ,& when i do somthing like this @foo stays nil , and foo is in message like self.message inside a class FooError like custom error class ... :`(
  • Jeff
    Jeff over 7 years
    How do you get the value of bla? (assume you rescued the exception, e): would e.bla work?
  • phoet
    phoet over 7 years
    the exception is just a plain ruby class. in order to get the value for :bla you would need to have a getter for @data and then access the hash key.
  • Jonathan
    Jonathan about 6 years
    Where is the best place to put this? /lib/errors.rb or somewhere in /app ?
  • phoet
    phoet about 6 years
    @Jonathan there is no best place for this. it depends on where and how you use that class. it's also perfectly valid to declare it alongside orwithin another class or namespace.
  • Lemon Cat
    Lemon Cat almost 5 years
    I think that in SuperWithMessageError, the message isn't set correctly because within the initialize method you are calling super(message) where message is nil, in which case StandardError's behavior is to set the message value to the class name. This explains why _ex_.message is 'SuperWithMessageError' in your example. If you changed the call to super to be super(foo), then the message would get set to whatever value of foo is provided.
  • Max Wallace
    Max Wallace almost 5 years
    @LemonCat that is all true but your solution isn't what the OP wants. To quote: "the raise syntax seems to only accept an Exception class and message, no optional parameters that get passed into the instantiation process." In other words, the OP is interested in an optional parameter that's not the message. Changing the call to super(foo) won't help because the goal is to have a parameter that's separate from the message.
  • Lemon Cat
    Lemon Cat almost 5 years
    @MaxWallace I follow you. The point I was trying to make was that when I read "In the second case, in SuperWithMessageError...the message doesn't get set correctly" is misleading: the message doesn't get set correctly because it is never initialized within #initialize(foo). For more context, please see stackoverflow.com/a/56371923/5299483.
  • Max Wallace
    Max Wallace almost 5 years
    @LemonCat your comment isn't correct. In SuperWithMessageError the message doesn't get set correctly because it's explicitly set to the wrong value (the return value of message before super is called, i.e. the class name) in #initialize. But what I think you are trying to get at, which is correct, is that in SuperError#initialize foo is implicitly passed in to super, which is why it works. I will update the answer and try to clarify that.
  • Lemon Cat
    Lemon Cat almost 5 years
    @MaxWallace, thanks for correcting my error. My understanding from reading the docs was that #message gets set to the class name if a message value is not provided to #initialize, but as you correctly point out even in the #initialize method of an Exception subclass, #message will return the class name until super(message) is called to set (well, really to override) the default message. TIL.
  • Max Wallace
    Max Wallace almost 5 years
    @LemonCat yeah, the docs on this could be much better. I also ended up reading the C source. Let me know if you think anything in my updated answer could be clearer!