Raise custom Exception with arguments

Ruby on-RailsRubyExceptionException Handling

Ruby on-Rails Problem Overview


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.

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

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 2 - Ruby on-Rails

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 3 - Ruby on-Rails

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 - Ruby on-Rails

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

Solution 5 - Ruby on-Rails

Simple pattern for custom errors with additional information

If the extra information you're looking to pass is simply a type with a message, this works well:

# define custom error class
class MyCustomError < StandardError; end

# raise error with extra information
raise MyCustomError, 'Extra Information'

The result (in IRB):

Traceback (most recent call last):
        2: from (irb):22
        1: from (irb):22:in `rescue in irb_binding'
MyCustomError (Extra Information)
Example in a class

The pattern below has become exceptionally useful for me (pun intended). It's clean, can be easily modularized, and the errors are expressive. Within my class I define new errors that inherit from StandardError, and I raise them with messages (for example, the object associated with the error).

Here's a simple example, similar to OP's original question, that raises a custom error within a class and captures the method name in the error message:

class MyUser
  # class errors
  class MyUserInitializationError < StandardError; end

  # instance methods
  def simulate_failure
    raise MyUserInitializationError, "method failed: #{__method__}"
  end
end

# example usage: 
MyUser.new.simulate_failure

# => MyUser::MyUserInitializationError (method failed: simulate_failure)

Solution 6 - Ruby on-Rails

It's counterintuitive for programmers coming from e.g. Java, but the most effective way to do this is not to write a custom initializer, but rather to write your own replacement for the Exception::exception class method.

Per the Kernel#raise docs:

> the first parameter should be an Exception class (or another object that returns an Exception object when sent an exception message). [Emphasis added.]

class MyException < StandardError

  class << self
    def exception(arg)
      # per `Exception::exception` docs
      return self if arg.nil? || self.equal?(arg)
      return MyException.new(arg.to_s) unless arg.is_a?(MyModel)

      # $! is a magic global variable holding the last raised
      # exception; Kernel#raise will also inject it as the 
      # cause attribute of the exception we construct here
      error_caught = $!
      msg = custom_message_for(arg, error_caught)

      ex = MyException.new(msg)
      # … any additional initialization goes here
      ex
    end

    private

    def custom_message_for(my_model_instance, error_caught)
      # …
    end
  end
end

This way, you can raise your custom exception normally, with a model instance instead of a string message, without having to remember to call new explicitly and upset RuboCop, as well as confusing Ruby programmers that come to your code later expecting the standard syntax.

begin
  my_model.frob
rescue => e
  raise MyException, my_model # works
end

raise MyException, 'some other reason' # also works

The message & initialization logic from MyException#exception could also go in a custom initializer, letting you just write MyException.new(arg, $!), but in that case make sure the initializer is smart enough to also handle a plain string message, and make sure it at some point calls super with a string message.

Solution 7 - Ruby on-Rails

You can create an new instance of your Exception subclass, then raise that. For instance:

begin
  # do something
rescue => e
  error = MyException.new(e, 'some info')
  raise error
end

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionChris KeeleView Question on Stackoverflow
Solution 1 - Ruby on-RailsphoetView Answer on Stackoverflow
Solution 2 - Ruby on-RailsMax WallaceView Answer on Stackoverflow
Solution 3 - Ruby on-RailscyrilchampierView Answer on Stackoverflow
Solution 4 - Ruby on-RailsLemon CatView Answer on Stackoverflow
Solution 5 - Ruby on-RailsMattView Answer on Stackoverflow
Solution 6 - Ruby on-RailsDavid MolesView Answer on Stackoverflow
Solution 7 - Ruby on-RailsEmilyView Answer on Stackoverflow