4 ways to early return from a rails controller

… and check why 5600+ Rails engineers read also this

4 ways to early return from a rails controller

When refactoring rails controllers you can stumble upon one gottcha. It’s hard to easily extract code into methods when it escapes flow from the controller method (usually after redirecting and sometimes after rendering). Here is an example:

1. redirect_to and return (classic)

class Controller
  def show
    unless @order.awaiting_payment? || @order.failed?
      redirect_to edit_order_path(@order) and return
    end

    if invalid_order?
      redirect_to tickets_path(@order) and return
    end

    # even more code over there ...
  end
end

So that was our first classic redirect_to and return way.

Let’s not think for a moment what we are going to do later with this code, whether some of it should landed in models or services. Let’s just tackle the problem of extracting it into a controller method.

2. extracted_method and return

class Controller
  def show
    verify_order and return
    # even more code over there ...
  end

  private

  def verify_order
    unless @order.awaiting_payment? || @order.failed?
      redirect_to edit_order_path(@order) and return true
    end

    if invalid_order?
      redirect_to tickets_path(@order) and return true
    end
  end
end

The problem with this technique is that after extracting the code into method you also need to fix all the returns so that they end with return true (instead of just return). If you forget about it you are going to introduce a new bug.

The other thing is that verify_order and return does not feel natural. When this method returns true I would rather expect the order to be positively verified so escaping early from controller action does not seem to make sense here.

So here is the alternative variant of it

2.b extracted_method or return

class Controller
  def show
    verify_order or return
    # even more code over there ...
  end

  private

  def verify_order
    unless @order.awaiting_payment? || @order.failed?
      redirect_to edit_order_path(@order) and return
    end

    if invalid_order?
      redirect_to tickets_path(@order) and return
    end

    return true
  end
end

Now it sounds better verify_order or return. Either the order is verified or we return early. If you decide to go with this type of refactoring you must remember to add return true at the end of the extracted method. However the good side is that all your redirect_to and return lines can remain unchanged.

3. extracted_method{ return }

class Controller
  def show
    verify_order{ return }
    # even more code over there ...
  end

  private

  def verify_order
    unless @order.awaiting_payment? || @order.failed?
      redirect_to edit_order_path(@order) and yield
    end

    if invalid_order?
      redirect_to tickets_path(@order) and yield
    end
  end
end

If we wanna return early from the top level method, why not be explicit about what we try to achieve. You can do that in Ruby if your callback block contains return. That way inner function can call the block and actually escape the outer function.

But when you look at verify_order method in isolation you won’t know that this yield is actually stopping the flow in verify_order as well. Next lines are not reached.

I don’t like when you need to look at outer function to understand the behavior of inner function. That’s completely contrary to what we usually try to achieve in programming by splliting code into methods that can be understood on their own and provide us with less cognitive burden.

4. extracted_method; return if performed?

class Controller
  def show
    verify_order; return if performed?
    # even more code over there ...
  end

  private

  def verify_order
    unless @order.awaiting_payment? || @order.failed?
      redirect_to edit_order_path(@order) and return
    end

    if invalid_order?
      redirect_to tickets_path(@order) and return
    end
  end
end

With ActionController::Metal#performed? you can test whether render or redirect already happended. This seems to be a good solution for cases when you extract code into method solely responsible for breaking the flow after render or redirect. I like it because in such case as shown, I don’t need to tweak the extracted method at all. The code can remain as it was and we don’t care about returned values from the subroutine.

throw :halt (sinatra bonus)

In sinatra you could use throw :halt for that purpose (don’t confuse throw (flow-control) with raise (exceptions)).

There was a discussion about having such construction in Rails a few years ago happening automagically for rendering and redirecting but the discussion is inconclusive and looks like it was not implemented in the end in rails.

It might be interesting for you to know that expecting render and redirect to break the flow of the method and exit it immediately is one of the most common mistake experienced by some Rails developers at the beginning of their career.

throw :halt (rails?)

As Avdi wrote and his blogpost Rack is also internally using throw :halt. However I am not sure if using this directly from Rails, deep, deep in your own controller code is approved and tesed. Write me a comment if you ever used it and it works correctly.

why not before filter?

Because in the end you probably want to put this code into service anyway and separate checking pre-conditions from http concerns.

You might also like