Raise custom Exception with arguments - ruby-on-rails

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.

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:
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

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

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

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)

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.

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

Related

How to determine where an exception is being rescued in Ruby?

Some background: I have some Ruby code in a large codebase (Rails) that raises an exception under certain conditions. The exception however does not "occur" as expected, it is silently discarded. I assume that some other code (a gem) rescues the exception, maybe accidentally.
How can I determine where that exception is being rescued?
I do have full control over the exception. So maybe there's a way for an exception to know when or that it is being rescued?
Contrived example:
# code outside my control
def foo
yield
rescue
end
def black_box(&block)
foo(&block)
end
# my code
black_box do
puts 'about to raise'
raise
puts 'never gets here'
end
Output:
about to raise
So the exception was rescued. How can I identify (from within "my code") that it was rescued in foo?
The only way I can think of (right now) is manual debugging/inspection.
When you're about to raise that exception you'd like to track, inspect current caller. This gives you a call stack. Now visit each line/method in your editor and look for rescues that are too much greedy.
As for more "automatic" ways, I don't see any. Ruby exceptions don't have on_rescue callbacks or anything like that, so they can't know they're being rescued.
[after 3 years, I'm answering my own question here]
At least in MRI, rescue is handled just like when in a case statement!
For example,
begin
# ...
rescue A, B
# ...
rescue C
# ...
rescue
# ...
end
is evaluated like:
case exception
when A, B
# ...
when C
# ...
when StandardError
# ...
end
Under the hood, both of the above execute: (until a match is found)
A === exception
B === exception
C === exception
StandardError === Exception
with === being Module#=== because A, B, C, and StandardError are classes.
That === call can be used to track down a potential rescue, e.g. by overriding Module#=== in Exception: (where it becomes a class method)
# foo.rb
def foo
yield
rescue
end
def black_box(&block)
foo(&block)
end
class Exception
def self.===(other)
cl = caller_locations[0]
puts "#{cl.path}:#{cl.lineno} - #{$!.inspect}"
super
end
end
require_relative 'foo.rb'
black_box do
puts 'about to raise'
raise 'error message'
puts 'never gets here'
end
Output:
about to raise
foo.rb:5 - #<RuntimeError: error message>
TracePoint could also be used to intercept the method call:
TracePoint.trace(:c_call) do |tp|
if tp.defined_class == Module && tp.method_id == :=== && tp.self <= Exception
puts "#{tp.path}:#{tp.lineno} - #{$!.inspect}"
end
end

Rescue nested method call

I read that Ruby doesn't have nested rescues. Is that why this won't work?
begin
dosomething
rescue => e
#this is never executed, dosomething raises the error directly
end
def dosomething
SomeModel.find(-1) #this raises the exception instead of above
end
Good day.
You must read ruby doc about exceptions
In short:
Exception - base class for All exceptions, therefore base code:
begin
# some code
rescue Exception => e
end
You should always specify which exception you handle
If you need proc all - use class Exception
P.S.
For you self education - http://rubylearning.com/satishtalim/ruby_exceptions.html

How to include Custom exception in Rails?

I don't understand well how Rails include (or not?) some file from the app directory.
For example, I've created a new directory app/exceptions for create my own exceptions. Now, from a helpers file, I want to raise one of my exception.
Am I suppose to include something in this helper?
The Helper: helpers/communications_helper.rb
//should I include something or it's suppose to be autoloaded?
module CommunicationsHelper
begin.
.
.
.
raise ParamsException, "My exception is lauch!"
rescue StandardError => e
...
end
end
The exception: exceptions/params_exception.rb
class ParamsException < StandardError
def initialize(object, operation)
puts "Dans paramsException"
end
end
Nothing specific from my raise in the output...
Thanks!
EDIT:
Thanks to all, your two answers was helpful in different way.
I didn't raise well the exception like you said, but I've also forggot to update my config.rb.
so now I 've:
rescue StandardError => e
raise ParamsError.new("truc", "truc")
Other question, do you know where can I catch the raise?
Cause I'm already in a catch block, so I'm little lost...
If you don't see output from your raise, make sure you're not rescuing the error by accident, since your error is a subclass of StandardError:
begin
raise ParamsException, "My exception is lauch!"
rescue StandardError => e # This also rescues ParamsException
end
As a side note, in Ruby it's common practice to have your custom errors end with Error rather than Exception. Unlike some other programming languages, classes ending with Exception are meant for system level errors.
First, I think that you're raising your exception incorrectly.
In your custom exception class, your initialize method takes in arguments. Therefore you should raise it with:
raise CustomError.new(arg1, arg2, etc.)
Read this.
Lastly, don't rescue from StandardError if CustomError is a child of StandardError; otherwise your manual 'raise' will be rescued.

How to create a ActiveRecord::RecordInvalid for testing?

I have this piece of code that I am trying to test:
def error_from_exception(ex)
if ex.is_a?(ActiveRecord::RecordInvalid)
...
To get into the if block, I need to pass in the correct ex param.
How do I create an ActiveRecord::RecordInvalid?
With rspec, I'm trying to do something like this:
context 'exception is ActiveRecord::RecordInvalid' do
it 'returns the validation error' do
begin
raise ActiveRecord::RecordInvalid
rescue Exception => ex
binding.pry
###
# From my pry session:
# $ ex
# $ <ArgumentError: wrong number of arguments (0 for 1)>
# $ ex.class
# $ ArgumentError < StandardError
###
end
end
end
How do I find out what argument type the library is looking for?
RecordInvalid link
None of the above methods worked for me so I finally did the following while doing a spec:
class InvalidRecord
include ActiveModel::Model
end
raise ActiveRecord::RecordInvalid.new(InvalidRecord.new)
Hope it helps!
EDIT: This is now possible in Rails 5.1.1. The record argument is no longer required after this commit: https://github.com/rails/rails/commit/4ff626cac901b41f86646dab1939d2a95b2d26bd
If you are on a Rails version under 5.1.1, see the original answer below:
It doesn't seem like it is possible to raise an ActiveRecord::RecordInvalid by itself. If you look at the source code for ActiveRecord::RecordInvalid, it requires a record when initializing:
class RecordInvalid < ActiveRecordError
attr_reader :record # :nodoc:
def initialize(record) # :nodoc:
#record = record
...
end
end
(source: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/validations.rb)
Something you could do to work around this would be to simply create an actual record that is invalid and attempt to save it using save! (such as calling User.new.save! when User.name is required). However, keep in mind that this might potentially become a problem in the future if the model you use is changed and it becomes valid inside your test (User.name is no longer required).
I needed to do something similar to test my code's handling of ActiveRecord::RecordInvalid. I wanted to do
allow(mock_ar_object).to receive(:save!).and_raise(ActiveRecord::RecordInvalid)
but that gives ArgumentError: wrong number of arguments (0 for 1) when RSpec tries to instantiate RecordInvalid.
Instead I wrote a RecordInvalid subclass and overrode initialize like this:
class MockRecordInvalid < ActiveRecord::RecordInvalid
def initialize
end
end
allow(mock_ar_object).to receive(:save!).and_raise(MockRecordInvalid)
Then a rescue ActiveRecord::RecordInvalid will catch MockRecordInvalid and MockRecordInvalid.new.is_a?(ActiveRecord::RecordInvalid) is true.
ActiveRecord::RecordInvalid needs an object to be created.
If you want to test just the exception itself, try:
null_object = double.as_null_object
ActiveRecord::RecordInvalid.new(null_object)
Understand the double as null object here (https://www.relishapp.com/rspec/rspec-mocks/v/2-6/docs/method-stubs/as-null-object)
I am using update_attribute(attr, 'value') instead of save! and I can mock the update_attribute method as follows:
expect(mock_object).to receive(:update_attribute).with(attr, 'value').and_raise(ActiveRecord::RecordInvalid)
We can stub it to the Model like this,
ModelName.any_instance.stubs(<method_name>).raises(ActiveRecord::RecordInvalid.new(record))
Example:
Post.any_instance.stubs(:save!).raises(ActiveRecord::RecordInvalid.new(post))

Store Exception Class Name for StandardError in Rails

I have defined a couple of custom exception classes that inherit from StandardError like so:
class InvalidPage < StandardError;end
If this exception is being raised I want to rescue it and store it to my DB for reference. So far I save its message and backtrace, but I can't seem to save its Name (InvalidPage) as well. I have tried:
InvalidPage.class
InvalidPage.class.name
InvalidPage.name
but none of the above works for me.
How can I access and save the Exception Name together with the message and backtrace?
You can do something like this:
class InvalidPage < StandardError; end
begin
raise InvalidPage
rescue InvalidPage => error
p error.class.to_s
p error.backtrace
end
# Output
# "InvalidPage"
# ["file.rb:4:in `<main>'"]

Resources