Raise custom Exception with arguments

exception, ruby, ruby-on-rails

Solution

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), 'argh'

You can get `foo` like this:

rescue FooError => error
  error.foo     # => 1234
  error.message # => 'argh'

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 great if `foo` is required. If you want `foo` to be an optional argument, then keep reading.

If you don't follow the Rubocop Style Guide

And want this to work:

raise FooError.new('argh', foo)

You need to pass the message to `super` as the only argument:

class FooError < StandardError
  attr_reader :foo

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

Explanation:

Pass your message as the second argument to `raise`

As the Rubocop Style Guide says, the message and the exception should be passed separately. If you write:

raise FooError.new('argh')

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

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

You 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. Sometimes this is very useful.

Why is this so complicated?

The crux of the problem is a design flaw in Ruby: exception messages get set in two different ways.

raise StandardError, 'argh'     # case 1
raise StandardError.new('argh') # case 2

In case 1, `raise` just calls `StandardError.new('argh')`, so these are the same. But what if you pass an exception instance and a message to `raise`?

raise FooError.new(foo), 'argh', backtrace

`raise` will set `'argh'` as the message on the `FooError` instance, so it behaves as if you called `super('argh')` in `FooError#initialize`.

We want to be able to use this syntax, because otherwise, we'll have to pass the message twice anytime we want to pass a backtrace:

raise FooError.new(foo, 'argh'), 'argh', backtrace
raise FooError.new('argh', foo), 'argh', backtrace 

But what if `foo` is optional? Then `FooError#initialize` is overloaded.

raise FooError, 'argh'          # case A
raise FooError.new(foo), 'argh' # case B

In case A, `raise` will call `FooError.new('argh')`, but your code expects an optional `foo`, not a message. This is bad. What are your options?

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

Don't use case A style. If you're not passing `foo`, write `raise FooError.new(), 'argh'`

Make foo a keyword argument

IMO, don't do 2. The code's not self-documenting, so you have to remember all of this. Too complicated.

If you don't want to use a keyword argument, my implementation of `FooError` way at the top of this answer actually works great with 1. This is why `FooError#initialize` has to call `super` and not `super()`. Because when you write `raise FooError, 'argh'`, `foo` will be `'argh'`, and you have to pass it to the parent class to set the message. The code doesn't break if you call `super` with something that isn't a string; nothing happens.

3 is the simplest option, if you're ok with a keyword argument - h/t Lemon Cat. Here's the code for that:

class FooError < StandardError
  attr_reader :foo

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

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

Problem

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.

Original source

Related problems