Why Rails Runs Code Before Your Controller Action
`before_action` is where a controller declares the request prerequisites that must run before action-specific code.
In the last article, we looked at how Rails builds params before the controller action runs.
Now the action has the request data.
But data is not the only thing an action needs.
Before a controller action can safely do its work, a Rails app often has to answer a few other questions:
Who is making this request?
Which account, tenant, project, or record does this request belong to?
Is this user allowed to continue?
Should this request be redirected before the action runs?
Is there a shared request context that the action should be able to assume?
Those questions rarely belong to a single action. They sit at the edge of many actions.
For example, a reporting action may look like this:
class ReportsController < ApplicationController
def index
@reports = current_account.reports.visible_to(current_user)
end
endThat action reads cleanly, but only because it assumes some things are already true:
there is a signed-in user
there is a current account
the current user is allowed to see reports for that account
If those things are not true, the action should probably not run at all.
before_action exists because controller actions usually need a place for request prerequisites.
The question for this article:
Why does Rails have a mechanism for running code before the controller action?
Every Action Has a Hidden Prologue
Imagine writing every controller action with all its prerequisites inline:
class ReportsController < ApplicationController
def index
redirect_to login_path and return unless current_user
@account = current_user.accounts.find(params[:account_id])
unless current_user.can_view_reports?(@account)
head :forbidden and return
end
@reports = @account.reports.visible_to(current_user)
end
endAs a single method, this is readable. You can go top to bottom and see the whole path.
The trouble starts when the same checks show up in show, new, create, update, destroy, and every custom action that belongs to the same part of the app.
So the controller starts to develop a repeated pattern:
make sure there is a user
find the current account
check access
load a record
then do the action-specific workRepetition is only part of the problem. The action-specific work also gets mixed with request-boundary work.
The action is supposed to answer a narrow question: “What should happen for this request?”
Before it can answer that, the application must establish the conditions under which the action may proceed.
Preconditions Before Action-Specific Work
before_action is Rails’ way of expressing controller preconditions.
It gives the controller a place to say: "Before this action runs, these conditions must be met or already prepared”.
So instead of repeating the same setup inside every action, you can write:
class ReportsController < ApplicationController
before_action :authenticate_user!
before_action :set_account
before_action :authorize_reports!
def index
@reports = @account.reports.visible_to(current_user)
end
endNow the action has a cleaner job. Authentication, account lookup, and request-boundary checks no longer crowd the method body.
It can assume the prerequisites declared by the controller above it.
The flow is:
request enters controller
->
preconditions and setup run
->
action-specific code runsbefore_action fits naturally into Rails controllers because controllers already operate around shared request boundaries.
They are not collections of unrelated Ruby methods. They are request handlers.
before_action is one way Rails lets you name those boundaries.
What Belongs Before an Action
Good before_action callbacks usually do one of three things.
They can protect the request:
before_action :authenticate_user!
before_action :require_admin!They can establish a request context:
before_action :set_locale
before_action :set_current_accountThey can load the state that several actions need:
before_action :load_project
before_action :load_invoice, only: [:show, :edit, :update]In each case, the callback answers a question that the action should not have to keep asking from scratch.
By the time this action runs:
def show
@line_items = @invoice.line_items.order(:created_at)
endthe controller has already established:
there is a current user
there is a current account
there is an invoice
the current user can reach itThe promise is useful, and it is exactly where callbacks can sometimes become dangerous.
The action looks small because some of the actual execution has moved elsewhere.
When the moved code is genuinely prerequisite work, the action gets cleaner. When the callback is doing the action’s real job in disguise, the controller gets harder to read.
A good practical rule:
Use
before_actionfor things the action should be able to assume, not for things the action is supposed to decide.
A readability rule more than a Rails rule, but it explains why some callbacks make a controller cleaner while others make the real behavior harder to find.
Why Callback Helpers Are Usually Private
One small Rails detail hides in most controller examples.
Callback methods are usually placed under private:
class ReportsController < ApplicationController
before_action :authenticate_user!
def index
@reports = Report.visible_to(current_user)
end
private
def authenticate_user!
redirect_to login_path unless current_user
end
endThe placement is intentional.
Rails treats public controller methods as possible actions. Internally, AbstractController::Base.action_methods builds the set of action names from public instance methods, after removing Rails’ own internal methods.
The visibility tells Rails and future readers which methods are endpoints and which are supporting code.
Since before_action sites near the top of the controller, right next to the action-facing surface, marking callback helpers private keeps the distinction clear:
public methods are actions
private methods support executionNot every controller method is meant to be reachable as an endpoint.
How Rails Turns That Idea Into Execution
When you write:
before_action :authenticate_user!Rails is not inserting a method call at the top of your action method. It is registering a callback around action processing.
The controller callback machinery lives in Action Pack, primarily through:
AbstractController::CallbacksThat module uses Rails’ general callback system:
ActiveSupport::CallbacksThe important internal shape is small:
def process_action(...)
run_callbacks(:process_action) do
super
end
endRails has a process_action step to execute controller actions, and callbacks are registered around it.
Conceptually, this:
before_action :authenticate_user!adds a before callback to the :process_action callback chain.
The source-shaped version is roughly:
set_callback(:process_action, :before, :authenticate_user!)The declaration at the top of the controller is not metadata. It becomes executable control flow around the action.
It is part of the framework path that leads to your action. The public API for this behavior is documented in AbstractController::Callbacks, AbstractController::Callbacks::ClassMethods, and ActiveSupport::Callbacks.
A Callback Can Decide the Action Should Not Run
When a precondition fails, before_action becomes more than setup: it can stop the action from running.
The most common example is authentication:
class ReportsController < ApplicationController
before_action :authenticate_user!
def index
@reports = Report.visible_to(current_user)
end
private
def authenticate_user!
redirect_to login_path unless current_user
end
endFor a signed-in user, the flow is:
authenticate user
->
indexFor a guest, the flow is:
authenticate user
->
redirect to login
->
index does not runThat behavior is the point.
If the request is not allowed to reach the action, the callback should be able to choose a response before the action-specific code runs.
In controller callbacks, rendering, redirecting, or calling head marks the response as already performed. Rails uses that to halt the normal path before the action runs.
The internal callback setup is built around that idea. The source-level shape checks whether the controller has already performed a response:
controller.performed?You may also see callbacks being halted with:
throw(:abort)That belongs to the broader Rails callback vocabulary, especially around model callbacks. For controller before_action debugging, the everyday path is usually more concrete: a callback rendered, redirected, or called head, so the controller has already performed a response.
From there comes a common debugging question:
Why is my action not running?
The answer is not always routing. It may be:
A before callback has already performed the response.
redirect_to Does Not Stop the Current Method
One small Ruby detail sits inside that controller behavior.
redirect_to can prevent the action from running, but it does not automatically stop Ruby from executing the rest of the current callback method.
The subtle bug appears here:
def authenticate_user!
redirect_to login_path unless current_user
AuditLoginAttempt.create!(user_agent: request.user_agent)
endIf current_user is missing, Rails will mark a redirect response as performed.
But Ruby can still continue to the next line inside authenticate_user!.
So if you want the callback method itself to stop, say so:
def authenticate_user!
return if current_user
redirect_to login_path
endor:
def authenticate_user!
redirect_to login_path and return unless current_user
endThe distinction:
redirect_to affects the controller response
return affects the Ruby methodRails uses the performed response to decide whether the action should continue. Ruby still uses ordinary method control flow inside the callback.
Order Matters Because Preconditions Depend on Each Other
Once you think of before_action as prerequisites, order becomes easier to reason about. Some prerequisites depend on earlier prerequisites.
This order makes sense:
class ProjectsController < ApplicationController
before_action :authenticate_user!
before_action :set_account
before_action :load_project
def show
end
private
def set_account
@account = current_user.accounts.find(params[:account_id])
end
def load_project
@project = @account.projects.find(params[:id])
end
endThe chain has a dependency:
authenticate_user!
->
set_account
->
load_project
->
showset_account assumes current_user.
load_project assumes @account.
The action assumes @project.
If you reverse the first two callbacks, the controller may try to find an account through a missing user. If you load the project before the account, the lookup has the wrong boundary.
Callback order is request behavior.
In complex apps, the chain often grows gradually. One callback is added for authentication. Another for tenancy. Another for a feature flag. Another for authorization. Eventually, the order is not just a list. It is a small execution graph written as a list.
When this list changes, behavior changes.
A Quick Word About around_action
Most controller callback discussions start with before_action, because it is the one you see most often.
Rails also has after_action, which runs action processing, and around_action, which wraps action processing.
For example:
around_action :measure_runtimeAn around_action is useful when the callback needs to surround the work rather than simply run before it.
The flow is close to:
around_action begins
->
action processing yields
->
around_action finishesThe exact order can depend on how the callbacks are declared, which is a deeper topic, perhaps for another time. For now, it is enough to see that these callbacks belong to the same controller execution story. Rails runs an action-processing chain, not just calls a method named after the action.
The Chain May Be Longer Than the Controller File
One reason callbacks surprise people is that the visible controller file may not contain the whole chain.
Most Rails apps put broad request prerequisites in ApplicationController:
class ApplicationController < ActionController::Base
before_action :set_locale
before_action :authenticate_user!
endThen a specific controller adds its own prerequisites:
class Admin::ReportsController < ApplicationController
before_action :require_admin!
before_action :load_report
def show
end
endThe local file shows:
require_admin!
load_report
showBut the request may actually pass through:
set_locale
authenticate_user!
require_admin!
load_report
showInheritance keeps broad request boundaries out of every individual controller. It also means an action can be affected by code that lives above it.
So, when debugging callbacks, do not read only the action’s file.
Read the controller ancestry:
ApplicationControllernamespace base controllers like
Admin::BaseControllerincluded controller concerns
authentication or authorization modules
any
skip_before_actionorprepend_before_action
The action may be local, but the chain is often inherited.
When a Controller Needs an Exception
Inherited callbacks are useful until one controller needs a different boundary.
Suppose ApplicationController requires authentication:
class ApplicationController < ActionController::Base
before_action :authenticate_user!
endMost controllers should inherit that.
But a public marketing page may need to skip it:
class PagesController < ApplicationController
skip_before_action :authenticate_user!, only: [:home]
def home
end
endThat is what skip_before_action is for. It removes a callback from the chain for the actions you specify.
The key here is “removes”.
It doesn’t pass authentication. It changes the callback chain so the authentication callback doesn’t run for that action.
Rails also gives you prepend_before_action when a callback must run before callbacks that were already registered.
prepend_before_action :set_current_tenantUse that sparingly, however. If callback order is already hard to see, prepending can make the chain even less local. But when a prerequisite must happen first, it is part of the callback toolbox.
Conditions Make Preconditions Selective
Not every action needs the same prerequisites.
Rails lets you write:
before_action :load_invoice, only: [:show, :edit, :update]or:
before_action :authenticate_user!, except: [:index, :show]Conceptually:
only: :showmeans:
if: ->(controller) { controller.action_name == "show" }So a callback can be present in the controller and still not apply to the action you are debugging.
There are two different failure modes:
the callback was not in the chain
the callback was in the chain, but its conditions did not match
Rails may have skipped it correctly.
A Practical Debugging Path
Suppose this request keeps redirecting to login:
GET /reportsYou expected:
ReportsController#indexto run, but your log line inside the action never appears.
The action may not have been the first application method Rails tried to run. Something earlier in the chain may have answered the request first.
The old question is: “Why did Rails not call my action?”
Ask instead: “What happened before the action?”
Walk the request in this order:
Did the route match the controller and action I expected?
->
Which before_action callbacks apply?
->
Are any inherited from ApplicationController or a parent controller?
->
Did one render, redirect, or call head?
->
Did the callback order cause an earlier failure?If you need to inspect the assembled callback chain, Rails exposes it:
ReportsController._process_action_callbacks.map do |callback|
[callback.kind, callback.filter]
endDo not build application behavior on that. Use it as a debugging flashlight.
It can show you what Rails has registered around process_action, including callbacks inherited from parent controllers.
The Trade-Off
before_action gives Rails controllers a clean way to express shared request prerequisites.
The cost is locality. The action body no longer tells the whole story.
There are request-boundary concerns:
before_action :authenticate_user!
before_action :set_current_account
before_action :load_projectThese start hiding behavior:
before_action :calculate_dashboard_metrics
before_action :choose_pricing_experiment
before_action :maybe_create_trial_subscriptionNow the callback list is doing more than preparing the request; it is hiding behavior the action probably ought to make visible.
The question is not:
Should I use callbacks or avoid them?
Instead, ask:
Is this code a prerequisite for the action, or is it the action’s real work?
If it is a prerequisite, before_action may be a good fit.
If it is the real work, hiding it in a callback makes the controller harder to read.
Outro
before_action exists because Rails actions often need shared preconditions.
The action should be able to say:
def show
@comments = @project.comments.recent
endwithout repeating every step required to make @project safe and meaningful.
Rails gives controllers a pre-action layer:
route matched
->
params assembled
->
controller instance prepared
->
before_action prerequisites
->
action-specific code
->
responseInternally, that layer is grounded in AbstractController::Callbacks, using ActiveSupport::Callbacks, around the process_action method.
Once you see callbacks as request preconditions rather than controller magic, the trade-offs become easier to evaluate: cleaner action bodies, less local control flow, and a chain you now know how to inspect.

