Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rollback not triggering in rails transaction when catching errors

I have some rails code in a controller action that has a transaction with a custom error.

When a speisfic error in a transaction happens I want to the transaction to rollback and render some json code, however I cannot seem to make the transaction rollback (it always commits) and then render some json. Here's my following code:

  def controller_action
    error_message = nil

    begin
      ActiveRecord::Base.transaction do
        code_that_moifys_db_to_rollback
        code_that_causes_custom_error
        some_more_code_i_dont_want_to_run
      end
    rescue SomeCustomError::Error
      error_message = 'this is an error message'
      raise ActiveRecord::Rollback
    end
  rescue ActiveRecord::Rollback
    # no rollback is getting triggered and data is now corrupt if json rendered
    render :json => {error: true, message: error_message}
    # however doing "raise 'xxxxx'" will cause a DB rollback in the transaction
  end

How do I both:

  • render json
  • and force the transaction to rollback

I think I may be misunderstanding how rollbacks are triggered, the strangest thing is if I raise in the second rescue it will trigger the transaction to rollback.

like image 511
MintDeparture Avatar asked Feb 02 '26 02:02

MintDeparture


1 Answers

I know I am too late to answer this question but some people might still be looking for an approach to its solution. So let's start with the problem and then head toward the solution.

In case you are in a hurry you can directly jump to the solution section.

Goal:

To render error response(other than saving object) and rollback transaction.

Problem context

In rails, if you have saving object errors(object.errors.present?) in a transaction then it will enter the rescue block (and you can handle raised exception in the rescue block) and your code will look something like this.

def controller_action
  ActiveRecord::Base.transaction do
    if object.save!
      render_success_response('success message here')
    else
       render_error_response(object.errors)
    end
  rescue => e
    render_error_response(e)
    raise ActiveRecord::Rollback
end

I will define two methods here to render error and success responses that I will be using.

def render_error_response(errors)
  render :json => {errors: errors}
end 


def render_success_response(success_message)
  render :json => {success_message: success_message}
end 

Now in the above code, you have to manually rollback the transaction but if you want rails to rollback transaction for you then you can write the above code like this

def controller_action
  begin
    ActiveRecord::Base.transaction do
      if object.save!
        render_success_response('success message here')
      else
         render_error_response(object.errors)
      end
    rescue => e
      render_error_response(e)
   end
 end

In the above code, rails will automatically rollback the transaction for you and will enter in the rescue block with an exception error.

Problem

But what if you have an errors array that comes in response to(call) a service or something and if errors are present in that array you want to render those errors and rollback the transaction? (as you know rails is not going to rollaback transaction for you this time as these errors are not saving object errors)

solution for versions below Rails 7

def controller_action
  begin
    ActiveRecord::Base.transaction do
      response = ResponseFromService
      if response[:errors].blank?
        render_success_response('success message here')
      else
         render_error_response(response[:errors])
         raise ActiveRecord::Rollback
      end
    rescue => e
      render_error_response(e)
   end
 end

Solution for Rails 7

In rails 7 we got a new feature that's exactly for this problem and that's using the return keyword so in rails 7 code will look something like this

def controller_action
  begin
    ActiveRecord::Base.transaction do
      response = ResponseFromService
      if response[:errors].blank?
        render_success_response('success message here')
      else
         return render_error_response(response[:errors])
      end
    rescue => e
      render_error_response(e)
   end
 end

Warning: Note that the return keyword will silently(without raising an exception ) rollbacks the transaction so be careful while using it.

like image 184
dev Avatar answered Feb 03 '26 15:02

dev