Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to bubble up exception in rails

New to rails and running into some issues around best practice error handling.

I have the following code in helper in the /lib folder which is used to apply a coupon to some pricing.

As you can see from code below i'm trying a few different ways to return any errors. Specifically, model.errors.add and flash[:error].

The code below is supposed to return a new price once a valid coupon has been applied. What i'm not sure about is what to return in the case of an error? I can't return false as I am below because this method should really be returning a numeric value, not a boolean. What is the best method to bubble up an error to the view from the lib/helper and stop execution?

def calc_coupon transaction_price, discount_code, current_tenant
   coupon = Coupon.where(code: discount_code, tenant_id: current_tenant.id).first

   if coupon.nil?
     current_tenant.errors.add :base, "No such coupon exists"
     return false
   else
     if coupon.maxed? || coupon.expired?
       flash[:error] = "This coupon is no longer valid."
       return false
     else
      transaction_price = coupon.calculate(transaction_price)
     end
   end
   return transaction_price
end
like image 390
cman77 Avatar asked Oct 26 '25 14:10

cman77


1 Answers

I think you are on the right track but you are struggling because you are mixing view logic with business logic. Forget the helper class and add the functionality to your model.

By adding the validations to the model rather than a helper class and adding the errors to base your controller action will be able to handle the errors when it tries to save/create the record in the normal way, thus "bubbling up the errors"

e.g.

    class MyModel < ActiveRecord ...
      validate :validate_coupon
      protected
        def validate_coupon
#          add your validations here - call your boolean check which adds errors to the model
#          if validations are ok then calculate your transaction price here
        end
    end

If you have your checks as public methods on your model then they can be called from anywhere, i.e. from within a view so a public transaction_price method on your model that first checks for errors and sets the price could be used in a view like so

<%= @some_object.transaction_price %>

As I stated earlier because the validations are added in the model then your controller action just needs to check the value of the call to save and flash the errors as normal if it returns false so no need to do anything custom in your controller actions at all. Just the default scaffolded behaviour.

There are a mountain of validation options available to you. Have a look at http://guides.rubyonrails.org/active_record_validations_callbacks.html

Hope that makes sense.

UPDATE IN RESPONSE TO COMMENT

1) You have that pretty much spot on. It is merely a suggestion that you could calculate the transaction price as part of the validation. If that doesn't suit your requirement then that's fine. I was just trying to give you the best fit for your question. The thing is that you should have two seperate instance methods then you can call them from wherever you wish which makes it a lot simpler for experimentation and moving stuff around. Go with your suggested solution It's good :)

2) Have a close look at a scaffolded controller and views. You will see how the saves/updates fail (They will fail automatically if you add errors as part of your validation process) using an if statement and you will see how the _form partial displays the errors

This is an example of an form that deals with engines in an app I am working on.

<%= form_for(@engine) do |f| %>
  <% if @engine.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@engine.errors.count, "error") %> prohibited this engine from being saved:</h2>

      <ul>
      <% @engine.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

Basically it is checking for errors and looping through them all if any exist to display them.

To handle flash messages you should utilise the applications main layout The following code will display flash messages inside a div called "info" which you can style however you want

  <% unless flash.empty? %>
    <div class="info">
    <%- flash.each do |name, msg| -%>
      <%= content_tag :div, msg, :id => "flash_#{name}" %>
    <%- end -%>
    </div>
  <% end %>

By placing that before the main yield in your application.html.erb in your views/layouts folder you can be sure that whatever page is rendered, flash messages will always be shown to the user

Of course if you are displaying flash messages then maybe you don;t need to display the errors. Sometimes both is the right thing to do and sometimes just one or the other is fine. That's up to you to decide on a case by case basis.

The main thing is that the receiving action for the HTTP POST or HTTP PUT (create or update action) handles the failure to save with or without a flash as you see fit e.g. A create action

  def create
    @engine = Engine.new(params[:engine])

    respond_to do |format|
      if @engine.save
        format.html { redirect_to @engine, notice: 'Engine was successfully created.' }
        format.json { render json: @engine, status: :created, location: @engine }
      else
        flash.now[:notice] = "Failed to save!" # flash.now because we are rendering. flash[:notice] if using a redirect
        format.html { render action: "new" }
        format.json { render json: @engine.errors, status: :unprocessable_entity }
      end
    end
  end

Hope that clears things up

UPDATE 2 Forgot to mention that flash is just a hash and uses the session cookie so you can set any key you like e.g.

flash.now[:fred]

This would enable you to style and handle a specific fred notification in a specific view if you wanted a different styling from that supplied by your application layout by adding the code to handle the :fred key (or name of your choice) in a specific view

like image 78
jamesc Avatar answered Oct 29 '25 11:10

jamesc



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!