How to Rescue Model Transaction and Show the User an Error

How to rescue model transaction and show the user an error?

def exchange_status_with(address)
ActiveRecord::Base.transaction do
self.save!
address.save!
end
rescue ActiveRecord::RecordInvalid => exception
# do something with exception here
end

FYI, an exception looks like:

#<ActiveRecord::RecordInvalid: Validation failed: Email can't be blank>

And:

exception.message
# => "Validation failed: Email can't be blank"

Side note, you can change self.save! to save!


Alternate solution if you want to keep your active model errors:

class MyCustomErrorClass < StandardError; end

def exchange_status_with(address)
ActiveRecord::Base.transaction do
raise MyCustomErrorClass unless self.save
raise MyCustomErrorClass unless address.save
end
rescue MyCustomErrorClass
# here you have to check self.errors OR address.errors
end

Error Handling in ActiveRecord Transactions?

If you are using the save! method with a bang (exclamation point), the application will throw an exception when the save fails. You would then have to catch the exception to handle the failure.

begin
@ticket.transaction do
@ticket.save!
@user.save!
end
#handle success here
rescue ActiveRecord::RecordInvalid => invalid
#handle failure here
end

How to rollback all transactions in transaction block on error in Ruby on Rails

This is insanely overcomplicated and you have completely missunderstood how to use nested attributes:

class MissionsController
def create
@mission = Mission.new(mission_attributes)
if @mission.save
redirect_to @mission
else
render :new
end
end

...

private

def mission_params
params.require(:mission)
.permit(
:param_1, :param_2, :param3,
addresses_attributes: [:foo, :bar, :baz],
phones_attributes: [:foo, :bar, :baz],
camera_spec_attributes: [:foo, :bar, :baz],
)
end
end

All the work is actually done automatically by the setters declared by accepts_nested_attributes. You just pass the hash or array of hashes of whitelisted parameters to it and let it do its thing.

You can prevent the parent object from being saved if the child object is invalid by using validates_associated:

class Mission < ApplicationRecord
# ...
validates_associated :addresses
end

This just adds the error key “Phone is invalid” to the errors which isn't very user friendly. If you want to display the error messages per nested record you can get the object wrapped by the form builder when using fields_for:

# app/shared/_errors.html.erb
<div id="error_explanation">
<h2><%= pluralize(object.errors.count, "error") %> prohibited this <%= object.model_name.singular %> from being saved:</h2>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
...
<%= f.fields_for :address_attributes do |address_fields| %>
<%= render('shared/errors', object: address_fields.object) if address_fields.object.errors.any? %>
<% end %>

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

@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.

Multi model saving, how to wrap in transaction and report errors

Let's start from the beginning.

We want our registration form object to have the same API as any other ActiveRecord model:

// view.html
<%= form_for(@book) do |f| %>
<% end %>

# controller.rb
def create
@book = Book.new(book_params)

if @book.save
redirect_to @book, notice: 'Book was successfully created.'
else
render :new
end
end

To do that, we create the following object:

class RegForm
include ActiveModel::Model

attr_accessor :company_name, :email, :password

def save
# Save Location and Account here
end
end

Now, by including ActiveModel::Model, our RegForm gains a ton of functionality, including showing errors and validating attributes (yes, it's unnecessary to include ActiveModel::Validations). In this next step we add some validations:

validates :email, :password, presence: true

And we change save so that it runs those validations:

def save
validate
# Save Location and Account here
end

validate returns true if all validations pass and false otherwise.

validate also adds errors to the @reg_form (All ActiveRecord models have an errors hash which is populated when a validation fails). This means that in the view we can do any of these:

@reg_form.errors.messages
#=> { email: ["can't be blank"], password: ["can't be blank"] }

@reg_form.errors.full_messages
#=> ["Email can't be blank", "Password can't be blank"]

@reg_form.errors[:email]
#=> ["can't be blank"]

@reg_form.errors.full_messages_for(:email)
#=> ["Email can't be blank"]

Meanwhile, our RegistrationsController should look something like this:

def create
@reg_form = RegForm.new(reg_params)

if @reg_form.save
redirect_to @reg_form, notice: 'Registration was successful'
else
render :new
end
end

We can clearly see that when @reg_form.save returns false, the new view is re-rendered.

Finally, we change save so that our models save calls are wrapped inside a transaction:

def save
if valid?
ActiveRecord::Base.transaction do
location = Location.create!(location_params)
account = location.create_account!(account_params)
end
true
end
rescue ActiveRecord::StatementInvalid => e
# Handle database exceptions not covered by validations.
# e.message and e.cause.message can help you figure out what happened
end

Points worthy of note:

  1. create! is used instead of create. The transaction is only rolled back if an exception is raised (which methods with a bang usually do).

  2. validate is just an alias for valid?. To avoid all that indentation we could instead use a guard at the top of the save method:

    return if invalid?
  3. We can turn a database exception (like an email uniqueness constraint) into an error by doing something like:

    rescue ActiveRecord::RecordNotUnique
    errors.add(:email, :taken)
    end
  4. We can add an error not directly associated with an attribute by using the symbol :base:

    errors.add(:base, 'Company and Email do not match')


Related Topics



Leave a reply



Submit