How to Inspect the Sequence of Controller Callbacks in Rails

How to Inspect the Sequence of Controller Callbacks in Rails

June 17, 2025

This post shows how to inspect the sequence of before, after, and around callbacks in Rails controllers by adding a small initializer. Useful for understanding callback order in applications with complex controller hierarchies or shared concerns. I learned this trick while reading the Rails tests.

While debugging a Rails app, sometimes it's useful to see the full list of callbacks that are set to run before, after, or around a controller action, in the exact sequence they will run. This is especially true in applications with a deep controller inheritance hierarchy or a heavy use of concerns with conditional callbacks.

Here's a small trick you can use to inspect the full list of callbacks for a controller. I just learned this today while trying to make sense of a confusing callback sequence in a fairly complex controller. As always, please let me know if there's a better or more idiomatic way to do this. 

First, add a new initializer with the following code under the initializers directory.

# config/initializers/callbacks.rb

class ActionController::Base
class << self
CALLBACK_KINDS = [:before, :after, :around].freeze

CALLBACK_KINDS.each do |kind|
define_method("#{kind}_actions") do
_process_action_callbacks.select { |c| c.kind == kind }.map(&:filter)
end
end
end
end

If you're not familiar with initializers, check out this post:

A Brief Introduction to Rails Initializers: Why, What, and How

The above initializer code loops through the supported kinds of callbacks and uses define_method to create a method for each one. Inside each method, it filters _process_action_callbacks by the specific kind and then maps to get the actual method names (or blocks) that will be invoked.

This dynamically defines three methods on every controller class:

  • before_actions
  • after_actions
  • around_actions

Each method returns an array of the corresponding callback methods for that controller, in the order they'll be invoked.

For example, here's the before_action method the above code will generate:

class ActionController::Base
class << self
def before_actions
filters = _process_action_callbacks.select { |c| c.kind == :before }
filters.map!(&:filter)
end
end
end

Usage

This is how you use the generated methods.

PostsController.before_actions
=>
[:verify_authenticity_token,
:set_site_info,
:configure_permitted_parameters,
:authenticate_user!,
:set_post,
:set_tags]

PostsController.after_actions
=> [:verify_same_origin_request]

PostsController.around_actions
=> [:turbo_tracking_request_id]

This tells you the exact sequence in which your callbacks are run. Very useful when debugging.

How it Works

Internally, Rails stores the list of action callbacks for each controller in a private method called _process_action_callbacks. Each item in that list is a Callback object with metadata like:

  • kind:before:after, or :around
  • filter: the method or block to be called
  • if / unless conditions (as Procs)

Even though it’s not part of the public API, it’s a clean and reliable way to introspect your controller’s filters for debugging or documentation purposes.

Now, often you'll hear that callbacks is a bad practice / anti-pattern. I personally find them very useful to manage side-logic not directly related to the action, just like concerns. Here's a fantastic video from DHH that shows callbacks in the wild, along with some practical and useful commentary on their usage.

On Writing Software Well #2: Using callbacks to manage auxiliary complexity

Reading the Rails Source Is Worth It.

Reading the Rails codebase, especially the tests, often leads to discovery of simple and useful patterns like this one.

I came across this trick while reading the Rails source code, looking for a built-in way to inspect the callback sequence. There wasn’t a ready-made method, but in filters_test.rb, I found some test code that did exactly what I needed. From there, it was just a matter of pulling it into an initializer and generalizing it.

If you're ever stuck, or wondering "is there a way to…", a quick dive into the source is often more productive than getting a ready-made answer online.

Sign up for my newsletter

Let's learn to become better developers.

Comments (2)

J
John Topley

Very useful, thanks Akshay!

W
Wagner Braga

Way above my level of understanding yet, however, inspire me to take a look at Rails source code.

Sign in to leave a comment.