Ruby on Rails: how do I catch ActiveRecord::Rollback? - ruby-on-rails

In my controller, I have code that looks like the following:
#mymodel.transaction do
for a in arr
#mymodel.some_method(a)
end
end
in #mymodel#some_method I could throw an ActiveRecord::Rollback exception which in the db does what it needs to do, however I then simply get an HTTP 500 and no way to catch the exception to let the user know in an elegant way what went wrong.
I've tried wrapping #mymodel.transaction do in a begin/rescue block, but that won't do it either. What's the best way to catch the exception so I can present the proper view to the user?

From the ActiveRecord::Base documentation:
Normally, raising an exception will cause the transaction method to rollback the database transaction and pass on the exception. But if you raise an ActiveRecord::Rollback exception, then the database transaction will be rolled back, without passing on the exception.
A small example:
class ThrowController < ApplicationController
def index
status = ActiveRecord::Base.connection.transaction do
raise ActiveRecord::Rollback.new
end
Rails.logger.info "followed transaction"
end
end
then:
>> c = ThrowController.new.index
=> "followed transaction \n"
As you can see, the ActiveRecord:::Rollback exception is swallowed by the transaction block.
It seems to me that something else is going on with your code that we're not aware of.

Related

How to know why a transaction was rolled back?

I have an Account model. It has usual validations and after_save callbacks. There is a requirement, and account objects are to be created following some additional validation strategy. I have the following snippet, which works fine:
def special_account_creation
Account.transaction do
a = Account.create(params)
resp = update_third_party(a) # Throws exception if unable to update
validate_amount(a, resp) # Throws exception if `a` is invalid
# rescue Rollback ??? <--- How to do this
# msg = <transaction_error_details>
# Rails.logger.info("Special Account not created due to: #{msg}")
end
end
a account is destroyed if anything goes wrong while calling a third party API or during validation.
I want to know how I can log the error to know why the transaction was rolled back.
ActiveRecord transaction catches any exception being raised inside and rolls back the transaction. After that, if the exception is ActiveRecord::Rollback, it is supressed otherwise any other exception is raised above the call stack.
Ref: https://api.rubyonrails.org/classes/ActiveRecord/Rollback.html
So you could do something like this:
Account.transaction do
begin
a = Account.create(params)
resp = update_third_party(a) # Throws exception if unable to update
validate_amount(a, resp) # Throws exception if `a` is invalid
rescue StandardError => e # <- assuming all errors need to be logged
Rails.logger.info("Special Account not created due to: #{msg}")
raise e # <-- Don't forget to raise the error!
end
end
I suggest raise the error from the rescue block - this will rollback the transaction and if the error is something other than ActiveRecord::Rollback, it will be raised outside the transaction block. These errors you can handle in rescue_from block in base controller.
Also prefer catching StandardError and not Exception: https://robots.thoughtbot.com/rescue-standarderror-not-exception
I am assuming we want to catch all possible errors in that block, hence the use of StandardError, otherwise use any specific errors that need to be intercepted.
ActiveRecord does not raise Rollback on failed update and/or failed validation.
You should rescue what was raised and then there are two opportunities: either raise Rollback to just rollback a transaction and continue normal execution flow, or re-raise what was raised to indeed break the normal flow.
def special_account_creation
Account.transaction do
a = Account.create(params)
begin
resp = update_third_party(a) # Throws exception if unable to update
validate_amount(a, resp) # Throws exception if `a` is invalid
rescue => e # might be more specific
Rails.logger.info("Transaction failed with a message #{e.message}")
raise ActiveRecord::Rollback, e.message # just to rollback
# raise e # for re-raising the exception
end
end
end

Correctly handling errors from a Database Transaction in your Rails Model

I have a method called invite! in my user model which will generate a unique token for a user and email them an invitation to join / sign up for my app.
The simplified implementation is -
class User < ActiveRecord::Base
# ...
# ...
def invite!
transaction do
generate_invitation_token
mark_as_invited
save!
end
WelcomeMailer.delay.send_invitation(id)
end
end
As you can see, it does some setup steps inside a transaction and then sends a WelcomeMailer invitation.
The key here is that I do NOT want an email delivered if there is any issue in updating the database during the transaction.
How do I best implement some error handling here?
The ruby-level method could throw an error. For example save! might error because of some model validation. In that case I can rescue it and return before it gets to the mailer.
What happens if there's some DB level transaction that errors out? Does it bubble up and throw an error in ruby? Will the same begin..rescue approach work there as well?
Thanks!
Two points about your case:
"RecordInvalid - raised by ActiveRecord::Base#save! and ActiveRecord::Base.create! when the record is invalid." (link)
"Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you should be ready to catch those in your application code." (link)
That means, what in a case of an error inside the transaction block in your current simplified code next things will happen:
the transaction will be rolled back
a mail will not be sent
the error will be throwed upper from invite!
If it's necessary to make some actions in a case of an error inside the transaction block, you may change the code like this:
def invite!
transaction do
generate_invitation_token
mark_as_invited
save!
end
rescue RecordInvalid => e
# some actions
raise e
else
# code that runs only if *no* exception was raised
WelcomeMailer.delay.send_invitation(id)
end

self.errors[:base] << inside an ActiveRecord transaction block

I want to update #some_instance unless the user does not meet some criteria. I put the criteria check and the #some_instance.save! in a transaction block so that if either of them fail, no transaction is made. This works well, but I am having trouble returning the correct error message. If the user does not meet the criteria, I want to return the reason why, OR if the #some_instance doesn't save, I want to return that error.
My code:
#some_controller.rb
begin
Some_Class.transaction do
return render json: { error: #user.errors }, status: :payment_required,
location: new_payment_path unless #user.meets_criteria
#some_instance.save!
end
rescue ActiveRecord::RecordInvalid => exception
render :json => { :error => exception.messages }, status: :unprocessable_entity
rescue => error
# handle some other exception
end
#User.rb
def meets_criteria
self.errors[:base] << "Does not meet criteria"
return false
end
The problem I'm facing is this: When the meets_criteria method returns false, I expect the return render json line to execute. Instead it catches an error in "rescue => error".
The return render json is never executed.
UPDATE:
#Gen suggested using a before_action instead of calling the meets_criteria in the transaction do block. I think this is a much better implementation, however I'm still curious why the return render is never called. Is it because ActiveRecord raises an error? If so shouldn't that be caught in the RecordInvalid exception?
#TheJKFever, I am not sure that your code supposed to jump to any rescue block (well, and I actually confirmed by running it).
Your some_validation method returns false, and therefore "unless #user.some_validation" evaluates to true, and the render is executed with the following log output:
Completed 402 Payment Required in 128ms
{"error":{"base":["Some error message"]}}
You can refer to ActiveRecord::RecordInvalid API for details about RecordInvalid. Namely, "Raised by save! and create! when the record is invalid".
So, your "rescue ActiveRecord::RecordInvalid => exception" is supposed to handle exceptions in the "#some_instance.save!" statement and not in your custom validation.
In your validation you don't actually have the code that raises the ActiveRecord::RecordInvalid exception and probably fails with another error, which is easy to check by outputing it in details.
In order to use some_validation with "self.errors[:base] <<" properly first your need to add the following statement to your user model:
validate :some_validation
In this case, if you call "#user.save!" instead of "#some_instance.save!", you would fall into that "rescue ActiveRecord::RecordInvalid => exception" block.
PS: #TheJKFever, I saw one of your comments below and wanted to clarify something. A validation has a well defined purpose to validate a model before saving, and what you need then is not a model validation. What you actually need is a before_action on your controller that will check that your user is ready to be used in such and such action (consider it as controller validation). And yes, you probably will need some method on your user model to do that check.
Updated (after question update)
#TheJKFever, as I mentioned earlier, when I implemented your code I was able to execute "return render json: { error: #user.errors }...". So, if it fails for you, it must be due to some exception during meets_criteria call, but it is not RecordInvalid exception. Since you wrapped meets_criteria into transaction, it means that it probably does some database changes that you want to rollback if #some_instance.save! was unsuccessful. It would be a good idea to wrap your meets_criteria with the same rescue blocks too and find out. Do you create! or save! something in meets_criteria? If so, then it also can throw RecordInvalid exception. The problem is that in your code RecordInvalid can be thrown by meets_criteria and by #some_instance.save!, and there is no way to see in the code which one. In any case, if you wrap your meets_criteria with rescue, you will be able to send your render errors request from it. And if you decide to go with before_action filter then you will have to move whole your transaction into it (assuming that it requires the integrity of the data).
The point is that ActiveRecord::RecordInvalid exception will only be thrown in case of save! or create! failure due to invalid data. It might not be the case in your code, and some other exception is thrown, and you end up in "rescue => error" block.
You are adding error to #user model and handling exception raised on #some_instance. Try #user.errors.messages[:base]
Custom validations aren't usually invoked via a public instance method. They're usually written as a private method and invoked by ActiveRecord during valid? if you register them in your model with, e.g. validate :some_validation.
In your case, the User model would need the following:
validate :some_validation
…
private
def some_validation
errors.add :base, "Some error message" if some_error_happens?
end
Then, in the controller, you would do:
Some_Class.transaction do
return render json: { error: #user.errors }, status: :payment_required,
location: new_payment_path if #user.valid?
#some_instance.save!
end

Non-fatal rescue in Rails

I'm trying to run a command that might fail sometimes. When it fails, it throws an exception.
What I'd like it to do is just log the error quietly and continue executing the next line below it, rather than aborting and going into the 'rescue' block. How should I approach this?
My current code is as follows:
rescue_from 'Gibbon::MailChimpError' do |exception|
logger.error("MAILCHIMP: #{exception}")
end
When I call the Mailchimp API, sometimes there is an error, and this disrupts the flow of my application. I just want it to carry on executing as if nothing has happened, and just note there was an error in the log.
How about something like this:
def rescuing(&block)
begin
yield
rescue NameError => e
puts "(Just rescued: #{e.inspect})"
end
end
rescuing do
puts "This is dangerous"
raise NameError
end
puts "... but I'm still alive"
Obviously, you'd have to replace NameError with the exception you want to be protected against.

Rails not catching exception in rescue block

User model has defined indexes to be searched using ThinkingSphinx. However when I stop my searchd deamon, I would like my method to fail gracefully and not throw an error. Normally I do this by using a rescue block for catching exceptions. But in this case, it still throws the error and the puts statement is never executed.
def search_users(key)
begin
search_results = User.search(key,options)
rescue Exception
puts "Hello World!!!"
search_results = []
end
return search_results
end
Following is the error i get:
Riddle::ConnectionError (Connection to 127.0.0.1 on 3201 failed. Connection refused - connect(2)):
Is there any way out?
Solved it.
Add the :populate => true option to your search calls.
Normally, Thinking Sphinx lazily loads search results (allowing for
sphinx scopes and such) - but if you want the rescue to take effect,
then you'll need to force the results to load immediately - hence the
:populate option.
Refer the link posted above for further reading.
Given ruby return semantics, you can compress your code:
def search_users(key)
begin
User.search(key,options)
rescue
puts "Hello World!!!"
[]
end
end
It is evil to rescue Exception. Just use rescue, which rescues StandardError, which captures most of the stuff you want it to. Otherwise you also capture SyntaxError, LoadError, SystemExit and other stuff you don't intend. In this case, rescue Riddle::ConnectionError is appropriate, but not necessary.

Resources