Is this an error-handling anti-pattern? - ruby-on-rails

A colleague recently told me this is an anti-pattern (record is an ActiveRecord):
begin
record.save!
do_something_else
rescue => e
puts "Unable to save"
end
...and I should do this instead:
if record.save
do_something_else
else
puts "Unable to save"
end
His argument is that I'm using an exception for flow control (which I agree is bad) but I believe this is a typical error-handling pattern.
Thoughts?

This is an antipattern, because there are better ways to check the validity of a record than running save! (which raises an error if it's not valid).
This is probably what your colleague was getting at:
if record.save
do_something_else
else
puts record.errors.full_messages
end
You see, there is just no benefit here of using the error as control flow because there are less roundabout ways to do it.
You can also run the validations independently of the save attempt. The errors.full_messages array (of strings) is populated when record.valid? gets called. record.save calls record.valid? internally.
if record.valid?
record.save # or save!; by this point you can assume the record is valid
else
puts record.errors.full_messages
end

Related

ActiveRecord::Base.transaction with Rails .save

It is my understanding that wrapping .save! in ActiveRecord::Base.transaction will make sure all the models (User, Profile, and Setting) to save together or none at all.
However, I was also told that including .save! with all the models.save! methods will also do that. So essentially, both version 1 and 2 are the same. I have a feeling I am wrong, so what is the difference?
Thank you
Version 1
def save
if valid?
ActiveRecord::Base.transaction do
User.save!
Profile.save!
Setting.save!
end
else
false
end
end
Version 2
def save
if valid?
User.save!
Profile.save!
Setting.save!
else
false
end
end
Reference: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
In the first case if any of save! statement fails then all the previous saved models will be rolled back. For ex:
If setting.save! fails then setting.save!, user.save! and profile.save! will be rolled back.
But in second case if any save! statement fails then it will only rollback that statement and it will also raise an exception. For ex:
If setting.save! fails then only setting.save! will be rolled back.
Both statements will work same only in 1 case when the first statement fails 'user.save!' as exception will be raised and in second case subsequent statement will not be executed
The difference between save and save! is that the latter will raise an exception but both will not save the value of object to table if validations fail.

Rspec test passing only when there's a PUTS in the model

The puts statement must be having some kind of weird effect that I'm not seeing here...
I have an Order model. There's a callback on the model where the callback requires the model to be fully committed; i.e., I need to use an after_commit. However, the determinant of if the callback should run or not requires ActiveRecord::Dirty and therefore requires a before_save (or after_save, but I use before_save based on some other non-essential info).
I have combined the two thusly:
class Order
# not stored in DB, used solely to help the before_save to after_commit transition
attr_accessor :calendar_alert_type, :twilio_alerter
before_save
if self.calendar_alert_type.nil?
if self.new_record?
self.calendar_alert_type = "create, both"
elsif self.email_changed?
self.calendar_alert_type = "update, both"
elsif self.delivery_start_changed? || self.delivery_end_changed? || (type_logistics_attributes_modified.include? "delivery")
self.calendar_alert_type = "update, start"
elsif self.pickup_start_changed? || self.pickup_end_changed? || (type_logistics_attributes_modified.include? "pickup")
self.calendar_alert_type = "update, end"
end
end
puts "whatever"
end
after_commit do
if self.calendar_alert_type.present?
calendar_alert(self.calendar_alert_type)
end
end
end
def calendar_alert(alert_info)
puts "whatever"
alert_type = alert_info.split(",")[0].strip
start_or_end = alert_info.split(",")[1].strip
if start_or_end == "both"
["start","end"].each do |which_end|
Calendar.send(alert_type, which_end, self.id)
end
else
Calendar.send(alert_type, start_or_end, self.id)
end
end
All of the private methods and the ActiveRecord::Dirty statements are working appropriately. This is an example of a spec:
it "email is updated" do
Calendar.should_receive(:send).with("update", "start", #order.id).ordered
Calendar.should_receive(:send).with("update", "end", #order.id).ordered
find("[name='email']").set("nes#example.com")
find(".submit-changes").click
sleep(1)
end
it "phone is updated" do
... #same format as above
end
Literally all the specs like the above pass ONLY when EITHER puts statements is present. I feel like I'm missing something very basic here, just can't put my finger on it. It's super weird because the puts statement is spitting out random text...
*Note, I'm totally aware that should_receive should be expect_to_receive and that I shouldn't use sleep and that expectation mocks on feature tests aren't good. Working on updating the specs separately from bad code days, but these shouldn't be causing this issue... (feel free to correct me)
This behavior depends on your Rails version. Before Rails 5 you can return anything except false value to keep on running. A false will abort the before_* callback chain. puts 'whatever' returns a nil. So every thing works. Your if block seems to return a false (custom implemation for calendar_alert_type?). In this case the chain is holded.
With Rails 5 you have to throw(:abort) to stop callback handling.

Capture all errors in form and ensure atomicity -rails

I want to capture all the errors in a form and also ensure that the operation is atomic that is the form goes through if there are no errors else it just captures the error and no other correct fields are saved.
ActiveRecord::Base.transaction do
#var_types.each_with_index do |var,index|
begin
var.save!
puts "saving"
rescue
puts "rescued"
end
end
I am using the above block, although this captures all the errors in but atomicity is not guarenteed. Any way to correct this?
EDIT:
Example
An example would be say something like a form where a user has to enter multiple fields and multiple fields may not conform to the db rules, so i expect all the errors to be shown at once so the save as to keep happening to get all the errors but at the same time ensure that no correct changes get saved if there is even one error.
You have to catch the exception outside the transaction.
The transactions are rollbacked when an exception goes out the transaction block
begin
ActiveRecord::Base.transaction do
#var_types.each_with_index do |var,index|
var.save!
puts "saving"
end
end
rescue
puts "rescued"
end
UPDATE after reading your comment:
ActiveRecord::Base.transaction do
raise ActiveRecord::Rollback unless #var_types.map(&:save).all? #passing here a block like { |res| res == true } is redundant.
redirect_to some_index_path, notice: "Everything saved"
end
render action: 'edit' # something didn't pass the validation, re-render the view
Things to note here:
If you raise ActiveRecord::Rollback inside a transaction block, you don't need a rescue for it (read the docs at http://api.rubyonrails.org/classes/ActiveRecord/Rollback.html)
Someone might say that you should not drive the flow based in exception-throwing, if you don't feel comfortable with it, you can do something like this:
all_saved = false # need to define this var outside, or it will a block-level variable, visible only in the block
ActiveRecord::Base.transaction do
all_saved = #var_types.map(&:save).all?
raise ActiveRecord::Rollback unless all_saved
end
if all_saved
redirect_to some_index_path, notice: "Everything saved"
else
render action: 'edit' # something didn't pass the validation, re-render the view
end

atomic transaction not working - ruby rails

So I am trying to achieve atomicitiy in while doing my saves (which are essentially updates on each rows).
params[:player_types].each do |p_type_params|
if p_type_params[:id]
player = #player_types.find(p_type_params[:id])
player.assign_attributes(p_type_params)
#player_types << player
end
end
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
if player_type.save
"DO Something.."
else
"DO something else.."
errors = true
end
end
end
Inspite of saving withing the transaction block, I can see partial saves also i.e. one of the rows get updated and the erroneous one does not (obviously) where as I would have wanted the updated row to be rolled back since there was atleast one row that could not be updated due to an error. Is my interpretation of transaction block correct in this case? How do I achieve an atomic save in my case?
EDIT: The model validates for uniqueness of one of the columns, which would be the reason for failing to update in the Database at this point.
You need to raise an error inside your transaction block to abort the transaction; setting errors doesn't impact the transaction.
For instance:
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
if player_type.save
"DO Something.."
else
"DO something else.."
raise "save failed!"
end
end
end
The more conventional way of doing this, of course, is to use save! which raises an exception for you when it fails:
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
player_type.save!
end
end
If you really need to "DO Something" on failure (besides aborting the transaction), you'll have to use the first method.

Refactor: Executing a block of code only if a there is no exception thrown

I've got a section of code that I would like to run only if the previous block of code doesn't throw an error. I've implemented a solution that seems hacky and I'm sure there is a better way to do it in Ruby.
Here's what I have so far:
existing_comments = Comment.all
catch(:creation_failure) do
begin
ActiveRecord::Base.transaction do
results.each do |row|
Comment.create!(row)
end
end
rescue
throw(:creation_failure)
end
existing_comments.destroy_all
end
There's gotta be a better way to do this.
It's very difficult to figure out what exactly it is that you are trying to do. As #ehabkost already pointed out, if an exception is raised, the execution is aborted anyway, so there's nothing you need to do. Anything which comes after the code that raised the exception won't be executed anyway, after all that is the whole point of exceptions.
Does this do what you want?
existing_comments = Comment.all
begin
ActiveRecord::Base.transaction do
results.each do |row|
Comment.create!(row)
end
end
rescue # You should *never* do this!
else
existing_comments.destroy_all
end
By the way: you should never, under no circumstances, just blindly rescue all exceptions. You should only ever rescue exactly the ones you expect. Do you really think it is a good idea to just blindly swallow, say, a ThreadError without ever noticing it? There are 39 direct subclasses of ActiveRecordError alone, perhaps one of those is more appropriate than just rescuing all exceptions (or at least all StandardError exceptions).
I'd propose this refactor:
Comment.transaction do
Comment.destroy_all
results.each do |row|
comment = Comment.new(row)
raise ActiveRecord::Rollback unless comment.save
end
end
I've moved the comments destroy to the top. While it's not exactly the same (new comments now won't clash against the existing ones), I think this makes more sense.
Note that throw/catch -useful as they are in certain scenarios- should not be used in normal coding or you'll end up with inextricable spaghetti code.
Try (I didn't get a chance to test it):
existing_comments = Comment.all
rescue Exception => e do
ActiveRecord::Base.transaction do
results.each do |row|
Comment.create!(row)
end
#you can log the failure messages, etc… by doing something with e
existing_comments.destroy_all
end
end
Transactions - http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html - ensure that the block executes as an atomic action. If an exception is raised, the entire transaction is rolledback.
I would do something like this:
existing_comments = Comment.all
begin
ActiveRecord::Base.transaction do
results.each do |row|
Comment.create!(row)
end
end
existing_comments.destroy_all
rescue Exception => e
# do something with the exception??
end
In your code you make it difficult (for me) by mixing raising exceptions (handled by rescue) and throwing something (handled by catch). Most of the times only raise is used.
Theory: raise/rescue is used to handle exceptions, throw/catch is used to control flow, and you can throw anything (not just exceptions). Unless really needed, i would avoid catch/throw.
Since when the exception is raised, the flow is interrupted, we can easily profit from that fact and only do the stuff if no exception occurred.
Hope this helps :)
When I read your question, I thought "wait, this is trivial: just write your code like you always do", e.g.:
def my_method
do_something
do_something_else
end
This way, do_something_else will run only if do_something doesn't raise an exception. You don't need to do anything special for that.
However, it looks like what you really want to do is to not run do_something_else and ignore the exception raised by do_something, right?
I have to note that you should be really careful if you are planning to ignore exceptions, but if you really want to do it, an easy way to ignore the exception from a block of code and not run anything else would be to simply return from the method. Like this:
def my_method
existing_comments = Comment.all
begin
ActiveRecord::Base.transaction do
results.each do |row|
Comment.create!(row)
end
end
rescue
# just return. ignore any exceptions from the block above
return
end
# this will run only if the above doesn't raise any exception:
existing_comments.destroy_all
end

Resources