Controller Callback Sequence in Rails

How to Inspect the Sequence of Controller Callbacks in Rails

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.

3 min read

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
At first glance, Rails initializers seem complex, but they’re solving a simple, but important problem: run some code after framework and gems are loaded, to initialize the application. This post covers the basics of initializers, including what they are, how they work, and how Rails implements them.

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.

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.


That's a wrap. I hope you found this article helpful and you learned something new.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.